

<!DOCTYPE html>
<html lang="zh-CN" data-default-color-scheme=&#34;auto&#34;>



<head>
  <meta charset="UTF-8">
  <link rel="apple-touch-icon" sizes="76x76" href="/crystal575/img/favicon.png">
  <link rel="icon" href="/crystal575/img/favicon.png">
  <meta name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  
    <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
  
  <meta name="theme-color" content="#2f4154">
  <meta name="description" content="Hello,here is crystal575&#39;s blog.">
  <meta name="author" content="crystal575">
  <meta name="keywords" content="">
  
  <title>后端开发面经 - crystal575&#39;s blog</title>

  <link  rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" />


  <link  rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@4.0.0/github-markdown.min.css" />
  <link  rel="stylesheet" href="/crystal575/lib/hint/hint.min.css" />

  
    
    
      
      <link  rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@10.7.2/styles/github-gist.min.css" />
    
  

  
    <link  rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css" />
  



<!-- 主题依赖的图标库，不要自行修改 -->

<link rel="stylesheet" href="//at.alicdn.com/t/font_1749284_ba1fz6golrf.css">



<link rel="stylesheet" href="//at.alicdn.com/t/font_1736178_kmeydafke9r.css">


<link  rel="stylesheet" href="/crystal575/css/main.css" />

<!-- 自定义样式保持在最底部 -->


  <script id="fluid-configs">
    var Fluid = window.Fluid || {};
    var CONFIG = {"hostname":"crystal575.gitee.io","root":"/crystal575/","version":"1.8.11","typing":{"enable":true,"typeSpeed":70,"cursorChar":"_","loop":false},"anchorjs":{"enable":true,"element":"h1,h2,h3,h4,h5,h6","placement":"right","visible":"hover","icon":""},"progressbar":{"enable":true,"height_px":3,"color":"#29d","options":{"showSpinner":false,"trickleSpeed":100}},"copy_btn":true,"image_zoom":{"enable":true,"img_url_replace":["",""]},"toc":{"enable":true,"headingSelector":"h1,h2,h3,h4,h5,h6","collapseDepth":0},"lazyload":{"enable":true,"loading_img":"/img/loading.gif","onlypost":false,"offset_factor":2},"web_analytics":{"enable":false,"baidu":null,"google":null,"gtag":null,"tencent":{"sid":null,"cid":null},"woyaola":null,"cnzz":null,"leancloud":{"app_id":null,"app_key":null,"server_url":null}},"search_path":"/local-search.xml"};
  </script>
  <script  src="/crystal575/js/utils.js" ></script>
  <script  src="/crystal575/js/color-schema.js" ></script>
<meta name="generator" content="Hexo 5.4.0"></head>


<body>
  <header style="height: 70vh;">
    <nav id="navbar" class="navbar fixed-top  navbar-expand-lg navbar-dark scrolling-navbar">
  <div class="container">
    <a class="navbar-brand"
       href="/crystal575/">&nbsp;<strong>crystal575's blog</strong>&nbsp;</a>

    <button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <div class="animated-icon"><span></span><span></span><span></span></div>
    </button>

    <!-- Collapsible content -->
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-auto text-center">
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/crystal575/">
                <i class="iconfont icon-home-fill"></i>
                首页
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/crystal575/archives/">
                <i class="iconfont icon-archive-fill"></i>
                归档
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/crystal575/categories/">
                <i class="iconfont icon-category-fill"></i>
                分类
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/crystal575/tags/">
                <i class="iconfont icon-tags-fill"></i>
                标签
              </a>
            </li>
          
        
          
          
          
          
            <li class="nav-item">
              <a class="nav-link" href="/crystal575/about/">
                <i class="iconfont icon-user-fill"></i>
                关于
              </a>
            </li>
          
        
        
          <li class="nav-item" id="search-btn">
            <a class="nav-link" target="_self" data-toggle="modal" data-target="#modalSearch">&nbsp;<i
                class="iconfont icon-search"></i>&nbsp;</a>
          </li>
        
        
          <li class="nav-item" id="color-toggle-btn">
            <a class="nav-link" target="_self">&nbsp;<i
                class="iconfont icon-dark" id="color-toggle-icon"></i>&nbsp;</a>
          </li>
        
      </ul>
    </div>
  </div>
</nav>

    <div class="banner" id="banner" parallax=true
         style="background: url('/crystal575/img/default.png') no-repeat center center;
           background-size: cover;">
      <div class="full-bg-img">
        <div class="mask flex-center" style="background-color: rgba(0, 0, 0, 0.3)">
          <div class="page-header text-center fade-in-up">
            <span class="h2" id="subtitle" title="后端开发面经">
              
            </span>

            
              <div class="mt-3">
  
    <span class="post-meta mr-2">
      <i class="iconfont icon-author" aria-hidden="true"></i>
      crystal575
    </span>
  
  
    <span class="post-meta">
      <i class="iconfont icon-date-fill" aria-hidden="true"></i>
      <time datetime="2021-11-05 15:39" pubdate>
        2021年11月5日 下午
      </time>
    </span>
  
</div>

<div class="mt-1">
  
    
    <span class="post-meta mr-2">
      <i class="iconfont icon-chart"></i>
      120.8k 字
    </span>
  

  
    
    <span class="post-meta mr-2">
      <i class="iconfont icon-clock-fill"></i>
      
      
      1290
       分钟
    </span>
  

  
  
    
      <!-- 不蒜子统计文章PV -->
      <span id="busuanzi_container_page_pv" style="display: none">
        <i class="iconfont icon-eye" aria-hidden="true"></i>
        <span id="busuanzi_value_page_pv"></span> 次
      </span>
    
  
</div>

            
          </div>

          
        </div>
      </div>
    </div>
  </header>

  <main>
    
      

<div class="container-fluid nopadding-x">
  <div class="row nomargin-x">
    <div class="d-none d-lg-block col-lg-2"></div>
    <div class="col-lg-8 nopadding-x-md">
      <div class="container nopadding-x-md" id="board-ctn">
        <div class="py-5" id="board">
          <article class="post-content mx-auto">
            <!-- SEO header -->
            <h1 style="display: none">后端开发面经</h1>
            
              <p class="note note-info">
                
                  本文最后更新于：2021年11月5日 下午
                
              </p>
            
            <div class="markdown-body">
              <h1>1. 操作系统</h1>
<h2 id="1-13-什么是协程？">1.13 什么是协程？</h2>
<p><strong>参考回答</strong></p>
<p><strong>协程</strong>：协程是微线程，在子程序内部执行，可在子程序内部中断，转而执行别的子程序，在适当的时候再返回来接着执行。</p>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p><strong>线程与协程的区别：</strong></p>
<p>（1）协程执行效率极高。协程直接操作栈基本没有内核切换的开销，所以上下文的切换非常快，切换开销比线程更小。</p>
<p>（2）协程不需要多线程的锁机制，因为多个协程从属于一个线程，不存在同时写变量冲突，效率比线程高。</p>
<p>（3）一个线程可以有多个协程。</p>
</li>
<li>
<p><strong>协程的优势：</strong></p>
<p>（1）<strong>协程调用跟切换比线程效率高</strong>：协程执行效率极高。协程不需要多线程的锁机制，可以不加锁的访问全局变量，所以上下文的切换非常快。</p>
<p>（2）<strong>协程占用内存少</strong>：执行协程只需要极少的栈内存（大概是4～5KB），而默认情况下，线程栈的大小为1MB。</p>
<p>（3）<strong>切换开销更少</strong>：协程直接操作栈基本没有内核切换的开销，所以切换开销比线程少。</p>
</li>
</ol>
<h2 id="1-14-为什么协程比线程切换的开销小？">1.14 为什么协程比线程切换的开销小？</h2>
<p><strong>参考回答</strong></p>
<p>（1）协程执行效率极高。协程直接操作栈基本没有内核切换的开销，所以上下文的<strong>切换非常快</strong>，切换开销比线程更小。</p>
<p>（2）协程不需要多线程的锁机制，因为多个协程从属于一个线程，不存在同时写变量冲突，效率比线程高。<strong>避免了加锁解锁的开销。</strong></p>
<h2 id="1-15-线程和进程的区别？">1.15 线程和进程的区别？</h2>
<p><strong>参考回答</strong></p>
<p>（1）一个线程从属于一个进程；一个进程可以包含多个线程。</p>
<p>（2）一个线程挂掉，对应的进程挂掉；一个进程挂掉，不会影响其他进程。</p>
<p>（3）进程是系统资源调度的最小单位；线程CPU调度的最小单位。</p>
<p>（4）进程系统开销显著大于线程开销；线程需要的系统资源更少。</p>
<p>（5）进程在执行时拥有独立的内存单元，多个线程共享进程的内存，如代码段、数据段、扩展段；但每个线程拥有自己的栈段和寄存器组。</p>
<p>（6）进程切换时需要刷新TLB并获取新的地址空间，然后切换硬件上下文和内核栈，线程切换时只需要切换硬件上下文和内核栈。</p>
<p>（7）通信方式不一样。</p>
<p>（8）进程适应于多核、多机分布；线程适用于多核</p>
<h2 id="1-16-进程切换为什么比线程更消耗资源？">1.16 进程切换为什么比线程更消耗资源？</h2>
<p><strong>参考回答</strong></p>
<p>进程切换时需要<strong>刷新TLB</strong>并获取新的地址空间，然后切换硬件上下文和内核栈；线程切换时只需要切换硬件上下文和内核栈。</p>
<p><strong>答案解析</strong></p>
<p><strong>进程是程序的动态表现。</strong> 一个程序进行起来后，会使用很多资源，比如使用寄存器，内存，文件等。每当切换进程时，必须要考虑保存当前进程的状态。状态包括存放在内存中的程序的代码和数据，它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开的文件描述符的集合，这个状态叫做上下文（Context）。可见，想要切换进程，保存的状态还不少。不仅如此，由于虚拟内存机制，进程切换时需要<strong>刷新TLB</strong>并获取新的地址空间。</p>
<p>线程存在于进程中，一个进程可以有一个或多个线程。<strong>线程是运行在进程上下文中的逻辑流</strong>，这个线程可以独立完成一项任务。同样线程有自己的上下文，包括唯一的整数线程ID， 栈、栈指针、程序计数器、通用目的寄存器和条件码。<strong>可以理解为线程上下文是进程上下文的子集。</strong></p>
<p><strong>由于保存线程的上下文明显比进程的上下文小，因此系统切换线程时，必然开销更小。</strong></p>
<h2 id="1-17-介绍一下进程之间的通信。">1.17 介绍一下进程之间的通信。</h2>
<p><strong>参考回答</strong></p>
<p>为了提高计算机系统的效率．增强计算机系统内各种硬件的并行操作能力．操作系统要求程序结构必须适应并发处理的需要．为此引入了进程的概念。而进程并行时，需要考虑进程间的通信，进程间通信主要有以下几种方式：</p>
<p><strong>匿名管道、命名管道、信号、消息队列、共享内存、信号量、Socket。</strong></p>
<ol>
<li>
<p>匿名管道：管道是一种半双工的通信方式，数据只能单向流动，而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。</p>
</li>
<li>
<p>有名管道</p>
<p>匿名管道，由于没有名字，只能用于亲缘关系的进程间通信。为了克服这个缺点，提出了有名管道(FIFO)。</p>
<p>有名管道不同于匿名管道之处在于它提供了一个路径名与之关联，<strong>以有名管道的文件形式存在于文件系统中</strong>，这样，<strong>即使与有名管道的创建进程不存在亲缘关系的进程，只要可以访问该路径，就能够彼此通过有名管道相互通信</strong>，因此，通过有名管道不相关的进程也能交换数据。值的注意的是，有名管道严格遵循<strong>先进先出(first in first out)</strong> ,对匿名管道及有名管道的读总是从开始处返回数据，对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。<strong>有名管道的名字存在于文件系统中，内容存放在内存中。</strong></p>
</li>
<li>
<p>信号</p>
</li>
</ol>
<ul>
<li>信号是Linux系统中用于进程间互相通信或者操作的一种机制，信号可以在任何时候发给某一进程，而无需知道该进程的状态。</li>
<li>如果该进程当前并未处于执行状态，则该信号就有内核保存起来，知道该进程回复执行并传递给它为止。</li>
<li>如果一个信号被进程设置为阻塞，则该信号的传递被延迟，直到其阻塞被取消是才被传递给进程。</li>
</ul>
<ol start="4">
<li>消息队列</li>
</ol>
<ul>
<li>消息队列是<strong>存放在内核中的消息链表</strong>，每个消息队列由消息队列标识符表示。</li>
<li>与管道（无名管道：只存在于内存中的文件；命名管道：存在于实际的磁盘介质或者文件系统）不同的是消息队列<strong>存放在内核中</strong>，只有在内核重启(即，操作系统重启)或者显示地删除一个消息队列时，该消息队列才会被真正的删除。</li>
<li>另外与管道不同的是，消息队列在某个进程往一个队列写入消息之前，并<strong>不需要另外某个进程在该队列上等待消息的到达</strong>。</li>
</ul>
<blockquote>
<p><strong>消息队列特点总结：</strong></p>
<p>（1）消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.</p>
<p>（2）消息队列允许一个或多个进程向它写入与读取消息.</p>
<p>（3）管道和消息队列的通信数据都是先进先出的原则。</p>
<p>（4）消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。</p>
<p>（5）消息队列克服了信号承载信息量少，管道只能承载无格式字 节流以及缓冲区大小受限等缺。</p>
<p>（6）目前主要有两种类型的消息队列：POSIX消息队列以及System V消息队列，系统V消息队列目前被大量使用。系统V消息队列是随内核持续的，只有在内核重起或者人工删除时，该消息队列才会被删除。</p>
</blockquote>
<ol start="5">
<li>
<p>共享内存</p>
<p>进程间本身的内存是相互隔离的，而共享内存机制相当于给两个进程开辟了一块二者均可访问的内存空间，这时，两个进程便可以共享一些数据了。但是，多进程同时占用资源会带来一些意料之外的情况，这时，我们往往会采用上述的信号量来控制多个进程对共享内存空间的访问。</p>
</li>
<li>
<p>信号量</p>
<p>信号量主要用来解决进程和线程间并发执行时的同步问题，进程同步是并发进程为了完成共同任务采用某个条件来协调他们的活动，这是进程之间发生的一种直接制约关系。</p>
<p>对信号量的操作分为P操作和V操作，P操作是将信号量的值减一，V操作是将信号量的值加一。当信号量的值小于等于0之后，再进行P操作时，当前进程或线程会被阻塞，直到另一个进程或线程执行了V操作将信号量的值增加到大于0之时。锁也是用的这种原理实现的。</p>
<p>信号量我们需要定义信号量的数量，设定初始值，以及决定何时进行PV操作。</p>
</li>
<li>
<p>socket</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/socket.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>套接字可以看做是：不同主机之间的进程进行双向通信的端点。（套接字 = IP地址 + 端口号）</p>
</li>
</ol>
<h2 id="1-19-说说僵尸进程和孤儿进程。">1.19 说说僵尸进程和孤儿进程。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>我们知道在unix/linux中，正常情况下，子进程是通过父进程创建的，子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后，它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。</li>
<li><strong>孤儿进程</strong>：一个父进程退出，而它的一个或多个子进程还在运行，那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养，并由init进程对它们完成状态收集工作。</li>
<li><strong>僵尸进程</strong>：一个进程使用fork创建子进程，如果子进程退出，而父进程并没有调用wait或waitpid获取子进程的状态信息，那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。</li>
</ol>
<h2 id="1-21-请介绍线程之间的通信方式。">1.21 请介绍线程之间的通信方式。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li><strong>锁机制</strong>：包括互斥锁、条件变量、读写锁互斥锁提供了以排他方式防止数据结构被并发修改的方法。读写锁允许多个线程同时读共享数据，而对写操作是互斥的。条件变量可以以原子的方式阻塞进程，直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。</li>
<li><strong>信号量机制(Semaphore)</strong>：包括无名线程信号量和命名线程信号量</li>
<li><strong>信号机制(Signal)</strong>：类似进程间的信号处理线程间的通信目的主要是用于线程同步，所以线程没有像进程通信中的用于数据交换的通信机制。</li>
</ol>
<h2 id="1-22-说一说进程的状态。">1.22 说一说进程的状态。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>进程的3种<strong>基本</strong>状态：<strong>运行、就绪和阻塞。</strong></li>
</ol>
<p>（1）<strong>就绪</strong>：当一个进程获得了除处理机以外的一切所需资源，一旦得到处理机即可运行，则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如，当一个进程由于时间片用完而进入就绪状态时，排入低优先级队列；当进程由I／O操作完成而进入就绪状态时，排入高优先级队列。</p>
<p>（2）<strong>运行</strong>：当一个进程在处理机上运行时，则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目，对于单处理机系统，处于运行状态的进程只有一个。在没有其他进程可以执行时（如所有进程都在阻塞状态），通常会自动执行系统的空闲进程。</p>
<p>（3）<strong>阻塞</strong>：也称为等待或睡眠状态，一个进程正在等待某一事件发生（例如请求I/O而等待I/O完成等）而暂时停止运行，这时即使把处理机分配给进程也无法运行，故称该进程处于阻塞状态。</p>
<p>其转移图如下：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E7%8A%B6%E6%80%81%E5%9B%BE.jpeg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<ol start="2">
<li>
<p>进程的五种状态</p>
<p><strong>创建状态</strong>：进程在创建时需要申请一个空白PCB，向其中填写控制和管理进程的信息，完成资源分配。如果创建工作无法完成，比如资源无法满足，就无法被调度运行，把此时进程所处状态称为创建状态</p>
<p><strong>就绪状态</strong>：进程已经准备好，已分配到所需资源，只要分配到CPU就能够立即运行</p>
<p><strong>执行状态</strong>：进程处于就绪状态被调度后，进程进入执行状态</p>
<p><strong>阻塞状态</strong>：正在执行的进程由于某些事件（I/O请求，申请缓存区失败）而暂时无法运行，进程受到阻塞。在满足请求时进入就绪状态等待系统调用</p>
<p><strong>终止状态</strong>：进程结束，或出现错误，或被系统终止，进入终止状态。无法再执行</p>
</li>
</ol>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E4%BA%94%E7%A7%8D%E7%8A%B6%E6%80%81.jpeg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h2 id="1-23-CPU调度的最小单位是什么？线程需要CPU调度吗？">1.23 CPU调度的最小单位是什么？线程需要CPU调度吗？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>进程是CPU分配资源的最小单位，线程是CPU调度的最小单位。</li>
<li>线程是比进程更小的能独立运行的基本单位，需要通过CPU调度来切换上下文，达到并发的目的。</li>
</ol>
<h2 id="1-24-进程之间共享内存的通信方式有什么好处？">1.24 进程之间共享内存的通信方式有什么好处？</h2>
<p><strong>参考回答</strong></p>
<p>采用共享内存通信的一个显而易见的好处是<strong>效率高</strong>，**因为进程可以直接读写内存，而不需要任何数据的拷贝。**对于像管道和消息队列等通信方式，则需要在内核和用户空间进行四次的数据拷贝，而共享内存则只拷贝两次数据：一次从输入文件到共享内存区，另一次从共享内存区到输出文件。</p>
<p>实际上，进程之间在共享内存时，并不总是读写少量数据后就解除映射，有新的通信时，再重新建立共享内存区域。而是保持共享区域，直到通信完毕为止，这样，数据内容一直保存在共享内存中，并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此，采用共享内存的通信方式效率是非常高的。</p>
<h2 id="1-25-如何杀死一个进程？">1.25 如何杀死一个进程？</h2>
<ol>
<li>
<p>杀死父进程并不会同时杀死子进程：每个进程都有一个父进程。可以使用 <code>pstree</code> 或 <code>ps</code> 工具来观察这一点。</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta">#</span><span class="bash"> 启动两个虚拟进程</span><br><span class="hljs-meta">$</span><span class="bash"> sleep 100 &amp;</span><br><span class="hljs-meta">$</span><span class="bash"> sleep 101 &amp;</span><br><span class="hljs-meta"></span><br><span class="hljs-meta">$</span><span class="bash"> pstree -p</span><br>init(1)-+<br>        |-bash(29051)-+-pstree(29251)<br>                      |-sleep(28919)<br>                      `-sleep(28964)<br><span class="hljs-meta"></span><br><span class="hljs-meta">$</span><span class="bash"> ps j -A</span><br> PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND<br>    0     1     1     1 ?           -1 Ss       0   0:03 /sbin/init<br>29051  1470  1470 29051 pts/2     2386 SN    1000   0:00 sleep 100<br>29051  1538  1538 29051 pts/2     2386 SN    1000   0:00 sleep 101<br>29051  2386  2386 29051 pts/2     2386 R+    1000   0:00 ps j -A<br>    1 29051 29051 29051 pts/2     2386 Ss    1000   0:00 -bash<br></code></pre></td></tr></table></figure>
<p>调用 <code>ps</code> 命令可以显示 PID（进程 ID） 和 PPID（父进程 ID）。</p>
<p>杀死父进程后，子进程将会成为孤儿进程，而 init 进程将重新成为它的父进程。</p>
</li>
<li>
<p>杀死进程组或会话中的所有进程</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta">$</span><span class="bash"> <span class="hljs-built_in">kill</span> -SIGTERM -- -19701</span><br></code></pre></td></tr></table></figure>
<p>这里用一个负数 <code>-19701</code> 向进程组发送信号。如果传递的是一个正数，这个数将被视为进程 ID 用于终止进程。如果传递的是一个负数，它被视为 PGID，用于终止整个进程组。负数来自系统调用的直接定义。</p>
<p>杀死会话中的所有进程与之完全不同。即使是具有会话 ID 的系统，例如 Linux，也没有提供系统调用来终止会话中的所有进程。需要遍历 <code>/proc</code> 输出的进程树，收集所有的 SID，然后一一终止进程。</p>
<p>Pgrep 实现了遍历、收集并通过会话 ID 杀死进程的算法。可以使用以下命令：</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">pkill -s &lt;SID&gt;<br></code></pre></td></tr></table></figure>
</li>
</ol>
<h2 id="1-26-说一说kill的原理。">1.26 说一说kill的原理。</h2>
<p><strong>参考回答</strong></p>
<ul>
<li>
<p>kill 命令的执行原理是这样的，kill 命令会向操作系统内核发送一个信号（多是终止信号）和目标进程的 PID，然后系统内核根据收到的信号类型，对指定进程进行相应的操作。kill 命令的基本格式如下：</p>
<p>[root@localhost ~]# kill [信号] PID</p>
</li>
<li>
<p>kill 命令是按照 PID 来确定进程的，所以 kill 命令只能识别 PID，而不能识别进程名。</p>
</li>
<li>
<p>kill 命令只是“发送”一个信号，因此，只有当信号被程序成功“捕获”，系统才会执行 kill 命令指定的操作；反之，如果信号被“封锁”或者“忽略”，则 kill 命令将会失效。</p>
</li>
</ul>
<h2 id="1-27-介绍下你知道的锁。">1.27 介绍下你知道的锁。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<h3 id="悲观锁">悲观锁</h3>
</li>
</ol>
<p>悲观锁并不是某一个锁，是一个锁类型，无论是否并发竞争资源，都会锁住资源，并等待资源释放下一个线程才能获取到锁。 这明显很悲观，所以就叫悲观锁。这明显可以归纳为一种策略，只要符合这种策略的锁的具体实现，都是悲观锁的范畴。</p>
<ol start="2">
<li>
<h3 id="乐观锁">乐观锁</h3>
</li>
</ol>
<p>与悲观锁相对的，乐观锁也是一个锁类型。当线程开始竞争资源时，不是立马给资源上锁，而是进行一些前后值比对，以此来操作资源。例如常见的CAS操作，就是典型的乐观锁。示例如下</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">cas</span><span class="hljs-params">(<span class="hljs-keyword">long</span> *addr, <span class="hljs-keyword">long</span> old, <span class="hljs-keyword">long</span> <span class="hljs-keyword">new</span>)</span> </span>&#123;<br>    <span class="hljs-comment">/* 原子执行 */</span><br>    <span class="hljs-keyword">if</span>(*addr != old)<br>        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>    *addr = <span class="hljs-keyword">new</span>;<br>    <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;<br>&#125;<br></code></pre></td></tr></table></figure>
<ol start="3">
<li>
<h3 id="自旋锁">自旋锁</h3>
</li>
</ol>
<p>自旋锁是一种基础的同步原语，用于保障对共享数据的互斥访问。与互斥锁的相比，在获取锁失败的时候不会使得线程阻塞而是一直自旋尝试获取锁。当线程等待自旋锁的时候，CPU不能做其他事情，而是一直处于轮询忙等的状态。</p>
<p>自旋锁主要适用于被持有时间短，线程不希望在重新调度上花过多时间的情况。实际上许多其他类型的锁在底层使用了自旋锁实现，例如多数互斥锁在试图获取锁的时候会先自旋一小段时间，然后才会休眠。如果在持锁时间很长的场景下使用自旋锁，则会导致CPU在这个线程的时间片用尽之前一直消耗在无意义的忙等上，造成计算资源的浪费。</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-comment">// 用户空间用 atomic_flag 实现自旋互斥</span><br><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;thread&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;vector&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;atomic&gt;</span></span><br><br>std::atomic_flag lock = ATOMIC_FLAG_INIT;<br><br><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">f</span><span class="hljs-params">(<span class="hljs-keyword">int</span> n)</span></span><br><span class="hljs-function"></span>&#123;<br>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> cnt = <span class="hljs-number">0</span>; cnt &lt; <span class="hljs-number">100</span>; ++cnt) &#123;<br>        <span class="hljs-keyword">while</span> (lock.<span class="hljs-built_in">test_and_set</span>(std::memory_order_acquire))  <span class="hljs-comment">// 获得锁</span><br>             ; <span class="hljs-comment">// 自旋</span><br>        std::cout &lt;&lt; <span class="hljs-string">&quot;Output from thread &quot;</span> &lt;&lt; n &lt;&lt; <span class="hljs-string">&#x27;\n&#x27;</span>;<br>        lock.<span class="hljs-built_in">clear</span>(std::memory_order_release);               <span class="hljs-comment">// 释放锁</span><br>    &#125;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    std::vector&lt;std::thread&gt; v;<br>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> n = <span class="hljs-number">0</span>; n &lt; <span class="hljs-number">10</span>; ++n) &#123;<br>        v.<span class="hljs-built_in">emplace_back</span>(f, n);<br>    &#125;<br>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">auto</span>&amp; t : v) &#123;<br>        t.<span class="hljs-built_in">join</span>();<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure>
<ol start="4">
<li>
<h3 id="公平锁">公平锁</h3>
</li>
</ol>
<p>多个线程竞争同一把锁，如果依照先来先得的原则，那么就是一把公平锁。</p>
<ol start="5">
<li>
<h3 id="非公平锁">非公平锁</h3>
</li>
</ol>
<p>多个线程竞争锁资源，抢占锁的所有权。</p>
<ol start="6">
<li>
<h4 id="共享锁">共享锁</h4>
</li>
</ol>
<p>多个线程可以共享这个锁的拥有权。一般用于数据的读操作，防止数据被写修改。共享锁的代码示例如下：</p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;shared_mutex&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;mutex&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;iostream&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;thread&gt;</span></span><br><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;chrono&gt;</span></span><br><br>std::shared_mutex test_lock;<br><br>std::mutex cout_lock;<br><br><span class="hljs-keyword">int</span> arr[<span class="hljs-number">3</span>] = &#123;<span class="hljs-number">11</span>, <span class="hljs-number">22</span>, <span class="hljs-number">33</span>&#125;;<br><br><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">unique_lock_demo</span><span class="hljs-params">(<span class="hljs-keyword">int</span> id)</span></span><br><span class="hljs-function"></span>&#123;<br>    std::unique_lock lock&#123;test_lock&#125;;<br><br>    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i =<span class="hljs-number">0</span>; i &lt; <span class="hljs-number">3</span>; i++)<br>    &#123;<br>            arr[i] = i + <span class="hljs-number">100</span> * id;<br>    &#125;<br><br>    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">3</span>; i++)<br>    &#123;<br>        <span class="hljs-function">std::unique_lock <span class="hljs-title">pl</span><span class="hljs-params">(cout_lock)</span></span>;<br>        std::cout &lt;&lt; <span class="hljs-string">&quot;In unique: &quot;</span> &lt;&lt; id &lt;&lt; <span class="hljs-string">&quot;: &quot;</span> &lt;&lt; arr[i] &lt;&lt; std::endl;<br>        pl.<span class="hljs-built_in">unlock</span>();<br>        std::this_thread::<span class="hljs-built_in">sleep_for</span>(std::chrono::<span class="hljs-built_in">seconds</span>(<span class="hljs-number">1</span>));<br>    &#125;<br>&#125;<br><br><br><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">shared_lock_demo</span><span class="hljs-params">(<span class="hljs-keyword">int</span> id)</span></span><br><span class="hljs-function"></span>&#123;<br>    std::shared_lock lock&#123;test_lock&#125;;<br><br>    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">3</span>; i++)<br>    &#123;<br>        <span class="hljs-function">std::unique_lock <span class="hljs-title">pl</span><span class="hljs-params">(cout_lock)</span></span>;<br>        std::cout &lt;&lt; <span class="hljs-string">&quot;In shared &quot;</span> &lt;&lt; id &lt;&lt; <span class="hljs-string">&quot;: &quot;</span> &lt;&lt; arr[i] &lt;&lt; std::endl;<br>        pl.<span class="hljs-built_in">unlock</span>();<br>        std::this_thread::<span class="hljs-built_in">sleep_for</span>(std::chrono::<span class="hljs-built_in">seconds</span>(<span class="hljs-number">1</span>));<br>    &#125;<br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br><br>   <span class="hljs-function">std::thread <span class="hljs-title">t3</span><span class="hljs-params">(unique_lock_demo,<span class="hljs-number">3</span>)</span></span>;<br>   <span class="hljs-function">std::thread <span class="hljs-title">t4</span><span class="hljs-params">(unique_lock_demo,<span class="hljs-number">4</span>)</span></span>;<br>   <span class="hljs-function">std::thread <span class="hljs-title">t1</span><span class="hljs-params">(shared_lock_demo,<span class="hljs-number">1</span>)</span></span>;<br>   <span class="hljs-function">std::thread <span class="hljs-title">t2</span><span class="hljs-params">(shared_lock_demo,<span class="hljs-number">2</span>)</span></span>;<br><br>   t1.<span class="hljs-built_in">join</span>();<br>   t2.<span class="hljs-built_in">join</span>();<br>   t3.<span class="hljs-built_in">join</span>();<br>   t4.<span class="hljs-built_in">join</span>();<br>   <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;<br>&#125;<br></code></pre></td></tr></table></figure>
<p>输出为：</p>
<figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">In</span> unique: <span class="hljs-number">3</span>: <span class="hljs-number">300</span><br><span class="hljs-attribute">In</span> unique: <span class="hljs-number">3</span>: <span class="hljs-number">301</span><br><span class="hljs-attribute">In</span> unique: <span class="hljs-number">3</span>: <span class="hljs-number">302</span><br><span class="hljs-attribute">In</span> shared <span class="hljs-number">1</span>: <span class="hljs-number">300</span><br><span class="hljs-attribute">In</span> shared <span class="hljs-number">2</span>: <span class="hljs-number">300</span><br><span class="hljs-attribute">In</span> shared <span class="hljs-number">1</span>: <span class="hljs-number">301</span><br><span class="hljs-attribute">In</span> shared <span class="hljs-number">2</span>: <span class="hljs-number">301</span><br><span class="hljs-attribute">In</span> shared <span class="hljs-number">1</span>: <span class="hljs-number">302</span><br><span class="hljs-attribute">In</span> shared <span class="hljs-number">2</span>: <span class="hljs-number">302</span><br><span class="hljs-attribute">In</span> unique: <span class="hljs-number">4</span>: <span class="hljs-number">400</span><br><span class="hljs-attribute">In</span> unique: <span class="hljs-number">4</span>: <span class="hljs-number">401</span><br><span class="hljs-attribute">In</span> unique: <span class="hljs-number">4</span>: <span class="hljs-number">402</span><br></code></pre></td></tr></table></figure>
<p>从这个输出可以看出：</p>
<ul>
<li>如果一个线程已经获取了<em>共享锁</em>，则其他任何线程都无法获取<em>互斥锁</em>，但是可以获取<em>共享锁</em>。</li>
<li>从这个输出可以看出，验证了如果一个线程已经获取了<em>互斥锁</em>，则其他线程都无法获取该锁。</li>
</ul>
<ol start="7">
<li>
<h3 id="死锁">死锁</h3>
</li>
</ol>
<p>死锁是指两个或两个以上的进程在执行过程中，由于竞争资源或者由于彼此通信而造成的一种阻塞的现象，若无外力作用，它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁，这些永远在互相等待的进程称为死锁进程。</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs c">mutex;   <span class="hljs-comment">//代表一个全局互斥对象</span><br><span class="hljs-function"><span class="hljs-keyword">void</span>  <span class="hljs-title">A</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    mutex.lock();<br>    <span class="hljs-comment">//这里操作共享数据</span><br>    B();  <span class="hljs-comment">//这里调用B方法</span><br>    mutex.unlock();<br>    <span class="hljs-keyword">return</span>;<br>&#125;<br><span class="hljs-function"><span class="hljs-keyword">void</span>  <span class="hljs-title">B</span><span class="hljs-params">()</span></span><br><span class="hljs-function"></span>&#123;<br>    mutex.lock();<br>    <span class="hljs-comment">//这里操作共享数据</span><br>    mutex.unlock();<br>    <span class="hljs-keyword">return</span>;<br>&#125;<br></code></pre></td></tr></table></figure>
<h2 id="1-28-什么情况下会产生死锁？">1.28 什么情况下会产生死锁？</h2>
<p><strong>参考回答</strong></p>
<p>如果在计算机系统中同时具备下面四个必要条件时，那么会发生死锁。换句话说，只要下面四个条件有一个不具备，系统就不会出现死锁。</p>
<p>1.<strong>互斥条件</strong>。即某个资源在一段时间内只能由一个进程占有，不能同时被两个或两个以上的进程占有。这种独占资源如CD-ROM驱动器，打印机等等，必须在占有该资源的进程主动释放它之后，其它进程才能占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源，两方的人不能同时过桥。</p>
<p>2.<strong>不剥夺条件</strong>。进程所获得的资源在未使用完毕之前，资源申请者不能强行地从资源占有者手中夺取资源，而只能由该资源的占有者进程自行释放。如过独木桥的人不能强迫对方后退，也不能非法地将对方推下桥，必须是桥上的人自己过桥后空出桥面（即主动释放占有资源），对方的人才能过桥。</p>
<p>3.<strong>请求和保持条件</strong>。进程至少已经占有一个资源，但又申请新的资源；由于该资源已被另外进程占有，此时该进程阻塞；但是，它在等待新资源之时，仍继续占用已占有的资源。还以过独木桥为例，甲乙两人在桥上相遇。甲走过一段桥面（即占有了一些资源），还需要走其余的桥面（申请新的资源），但那部分桥面被乙占有（乙走过一段桥面）。甲过不去，前进不能，又不后退；乙也处于同样的状况。</p>
<p>4.<strong>循环等待条件</strong>。存在一个进程等待序列{P1，P2，…，Pn}，其中P1等待P2所占有的某一资源，P2等待P3所占有的某一源，…，而Pn等待P1所占有的的某一资源，形成一个进程循环等待环。就像前面的过独木桥问题，甲等待乙占有的桥面，而乙又等待甲占有的桥面，从而彼此循环等待。</p>
<p>上面提到的这四个条件在死锁时会同时发生。也就是说，只要有一个必要条件不满足，则死锁就可以排除。</p>
<h2 id="1-29-说一说你对自旋锁的理解。">1.29 说一说你对自旋锁的理解。</h2>
<p><strong>参考回答</strong></p>
<p>旋锁的定义：当一个线程尝试去获取某一把锁的时候，如果这个锁此时已经被别人获取(占用)，那么此线程就无法获取到这把锁，该线程将会等待，间隔一段时间后会再次尝试获取。这种采用循环加锁 -&gt; 等待的机制被称为<code>自旋锁(spinlock)</code>。</p>
<p><strong>自旋锁有以下特点</strong></p>
<ul>
<li>用于临界区互斥</li>
<li>在任何时刻最多只能有一个执行单元获得锁</li>
<li>要求持有锁的处理器所占用的时间尽可能短</li>
<li>等待锁的线程进入忙循环</li>
</ul>
<p><strong>自旋锁存在的问题</strong></p>
<ul>
<li>如果某个线程持有锁的时间过长，就会导致其它等待获取锁的线程进入循环等待，消耗CPU。使用不当会造成CPU使用率极高。</li>
<li>无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。</li>
</ul>
<p><strong>自旋锁的优点</strong></p>
<ul>
<li>自旋锁不会使线程状态发生切换，一直处于用户态，即线程一直都是active的；不会使线程进入阻塞状态，减少了不必要的上下文切换，执行速度快</li>
<li>非自旋锁在获取不到锁的时候会进入阻塞状态，从而进入内核态，当获取到锁的时候需要从内核态恢复，需要线程上下文切换。（线程被阻塞后便进入内核（Linux）调度状态，这个会导致系统在用户态与内核态之间来回切换，严重影响锁的性能）</li>
</ul>
<p><strong>自旋锁与互斥锁的区别</strong></p>
<ul>
<li>自旋锁与互斥锁都是为了实现保护资源共享的机制。</li>
<li>无论是自旋锁还是互斥锁，在任意时刻，都最多只能有一个保持者。</li>
<li>获取互斥锁的线程，如果锁已经被占用，则该线程将进入睡眠状态；获取自旋锁的线程则不会睡眠，而是一直循环等待锁释放。</li>
</ul>
<h2 id="1-30-说一说你对悲观锁的理解。">1.30 说一说你对悲观锁的理解。</h2>
<p><strong>参考回答</strong></p>
<p>悲观锁总是假设最坏的情况，每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这样别人想拿这个数据就会阻塞直到它拿到锁（<strong>共享资源每次只给一个线程使用，其它线程阻塞，用完后再把资源转让给其它线程</strong>）。传统的关系型数据库里边就用到了很多这种锁机制，比如行锁，表锁等，读锁，写锁等，都是在做操作之前先上锁。</p>
<h2 id="1-31-说一说你对乐观锁的理解。">1.31 说一说你对乐观锁的理解。</h2>
<p><strong>参考回答</strong></p>
<p>乐观锁总是假设最好的情况，每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在更新的时候会判断一下在此期间别人有没有去更新这个数据，可以使用版本号机制和CAS算法实现。<strong>乐观锁适用于多读的应用类型，这样可以提高吞吐量</strong>，像数据库提供的类似于<strong>write_condition机制</strong>，其实都是提供的乐观锁。</p>
<h2 id="1-32-CAS在什么地方用到过吗？">1.32 CAS在什么地方用到过吗？</h2>
<p><strong>参考回答</strong></p>
<ul>
<li>
<p>CAS是英文单词<strong>CompareAndSwap</strong>的缩写，中文意思是：比较并替换。CAS需要有3个操作数：内存地址V，旧的预期值A，即将要更新的目标值B。CAS指令执行时，当且仅当内存地址V的值与预期值A相等时，将内存地址V的值修改为B，否则就什么都不做。整个比较并替换的操作是一个原子操作。</p>
</li>
<li>
<p>高并发环境下，对同一个数据的<strong>并发读</strong>（两边都读出余额是100）与<strong>并发写</strong>（一个写回28，一个写回38）导致的数据一致性问题。</p>
<p>解决方案是在set写回的时候，加上初始状态的条件compare，只有初始状态不变时，才允许set写回成功，这是一种常见的降低读写锁冲突，保证数据一致性的方法。</p>
</li>
</ul>
<h2 id="1-33-谈谈IO多路复用。">1.33 谈谈IO多路复用。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<h3 id="IO多路复用是一种同步IO模型">IO多路复用是一种<strong>同步IO模型</strong></h3>
<p>实现一个线程可以监视多个文件句柄；一旦某个文件句柄就绪，就能够通知应用程序进行相应的读写操作；没有文件句柄就绪时会阻塞应用程序，交出cpu。<strong>多路是指网络连接，复用指的是同一个线程。</strong></p>
</li>
<li>
<h3 id="IO多路复用有三种实现方式-select-poll-epoll">IO多路复用有三种实现方式:select, poll, epoll</h3>
<p>(1)<strong>select</strong>：时间复杂度O(n)，它仅仅知道了，有I/O事件发生了，却并不知道是哪那几个流（可能有一个，多个，甚至全部），只能无差别轮询所有流，找出能读出数据，或者写入数据的流，对他们进行操作。所以<strong>select具有O(n)的无差别轮询复杂度</strong>，同时处理的流越多，无差别轮询时间就越长。</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">select</span> <span class="hljs-params">(<span class="hljs-keyword">int</span> n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)</span></span>;<br></code></pre></td></tr></table></figure>
<p>**select 函数监视的文件描述符分3类，分别是writefds、readfds、和exceptfds。**调用后select函数会阻塞，直到有描述副就绪（<strong>有数据 可读、可写、或者有except</strong>），或者超时（timeout指定等待时间，如果立即返回设为null即可），函数返回。<strong>当select函数返回后，可以 通过遍历fdset，来找到就绪的描述符。</strong></p>
<p>select目前几乎在所有的平台上支持，其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制，<strong>在Linux上一般为1024</strong>，可以通过修改宏定义甚至重新编译内核的方式提升这一限制，但是这样也会造成效率的降低。</p>
</li>
</ol>
<p>(2)<strong>poll</strong>：<strong>时间复杂度O(n)</strong>，poll本质上和select没有区别，它将用户传入的数组拷贝到内核空间，然后查询每个fd对应的设备状态， <strong>但是它没有最大连接数的限制</strong>，原因是它是<strong>基于链表来存储</strong>的。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">int poll (struct pollfd *fds, unsigned int nfds, int timeout);<br></code></pre></td></tr></table></figure>
<p>不同与select使用三个位图来表示三个fdset的方式，poll使用一个 pollfd的指针实现。</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">pollfd</span> &#123;</span>    <span class="hljs-keyword">int</span> fd; <span class="hljs-comment">/* file descriptor */</span>    <span class="hljs-keyword">short</span> events; <span class="hljs-comment">/* requested events to watch */</span>    <span class="hljs-keyword">short</span> revents; <span class="hljs-comment">/* returned events witnessed */</span>&#125;;<br></code></pre></td></tr></table></figure>
<p>pollfd结构包含了要监视的event和发生的event，不再使用select“参数-值”传递的方式。同时，pollfd并没有最大数量限制（但是数量过大后性能也是会下降）。 <strong>和select函数一样，poll返回后，需要轮询pollfd来获取就绪的描述符。</strong></p>
<p>(3)<strong>epoll</strong>：<strong>时间复杂度O(1)</strong>，<strong>epoll可以理解为event poll</strong>，<strong>不同于忙轮询和无差别轮询，epoll会把哪个流发生了怎样的I/O事件通知我们</strong>。所以说epoll实际上是<strong>事件驱动（每个事件关联上fd）</strong> 的，此时对这些流的操作都是有意义的。</p>
<p>epoll操作过程需要三个接口，分别如下：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">int epoll_create(int size)；//创建一个epoll的句柄，size用来告诉内核这个监听的数目一共有多大int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)；int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);<br></code></pre></td></tr></table></figure>
<ul>
<li>int epoll_create(int size)：创建一个epoll的句柄，size用来告诉内核这个监听的数目一共有多大，这个参数不同于select()中的第一个参数，给出最大监听的fd+1的值，参数size并不是限制了epoll所能监听的描述符最大个数，只是对内核初始分配内部数据结构的一个建议。当创建好epoll句柄后，它就会占用一个fd值，在linux下如果查看/proc/进程id/fd/，是能够看到这个fd的，所以在使用完epoll后，必须调用close()关闭，否则可能导致fd被耗尽。</li>
<li>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)：函数是对指定描述符fd执行op操作。- epfd：是epoll_create()的返回值。- op：表示op操作，用三个宏来表示：添加EPOLL_CTL_ADD，删除EPOLL_CTL_DEL，修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。- fd：是需要监听的fd（文件描述符）- epoll_event：是告诉内核需要监听什么事，</li>
<li>int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)：等待epfd上的io事件，最多返回maxevents个事件。参数events用来从内核得到事件的集合，maxevents告之内核这个events有多大，这个maxevents的值不能大于创建epoll_create()时的size，参数timeout是超时时间（毫秒，0会立即返回，-1将不确定，也有说法说是永久阻塞）。该函数返回需要处理的事件数目，如返回0表示已超时。</li>
</ul>
<ol start="3">
<li>
<h3 id="select、poll、epoll区别">select、poll、epoll区别</h3>
</li>
</ol>
<ul>
<li>支持一个进程所能打开的最大连接数</li>
</ul>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<ul>
<li>FD剧增后带来的IO效率问题</li>
</ul>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<ul>
<li>消息传递方式</li>
</ul>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h2 id="1-34-谈谈poll和epoll的区别。">1.34 谈谈poll和epoll的区别。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>poll将用户传入的数组拷贝到内核空间，然后查询每个fd对应的设备状态，如果设备就绪则在设备等待队列中加入一项并继续遍历，如果遍历完所有fd后没有发现就绪设备，则挂起当前进程，直到设备就绪或者主动超时，被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。</p>
<p>它没有最大连接数的限制，原因是它是基于链表来存储的，但是同样有缺点：</p>
<ol>
<li><code>大量的fd的数组被整体复制于用户态和内核地址空间之间</code>，而不管这样的复制是不是有意义。</li>
<li><code>poll还有一个特点是“水平触发”</code>，如果报告了fd后，没有被处理，那么下次poll时会再次报告该fd。</li>
</ol>
</li>
<li>
<p>epoll是在2.6内核中提出的，是之前的select和poll的增强版本。相对于select和poll来说，epoll更加灵活，没有描述符限制。epoll使用一个文件描述符管理多个描述符，将用户关系的文件描述符的事件存放到内核的一个事件表中，这样在用户空间和内核空间的copy只需一次。</p>
<p>epoll支持水平触发和边缘触发，最大的特点在于边缘触发，它只告诉进程哪些fd刚刚变为就绪态，并且只会通知一次。还有一个特点是，epoll使用“事件”的就绪通知方式，通过epoll_ctl注册fd，一旦该fd就绪，内核就会采用类似callback的回调机制来激活该fd，epoll_wait便可以收到通知。其优点有：</p>
<ol>
<li>没有最大并发连接的限制，能打开的FD的上限远大于1024（1G的内存上能监听约10万个端口）。</li>
<li>效率提升，不是轮询的方式，不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数；即Epoll最大的优点就在于它只管你“活跃”的连接，而跟连接总数无关，因此在实际的网络环境中，Epoll的效率就会远远高于select和poll。</li>
<li>内存拷贝，利用mmap()文件映射内存加速与内核空间的消息传递；即epoll使用mmap减少复制开销。</li>
</ol>
</li>
</ol>
<h2 id="1-35-谈谈select和epoll的区别。">1.35 谈谈select和epoll的区别。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>select 函数监视的文件描述符分3类，分别是writefds、readfds、和exceptfds。调用后select函数会阻塞，直到有描述符就绪（有数据 可读、可写、或者有except），或者超时（timeout指定等待时间，如果立即返回设为null即可），函数返回。当select函数返回后，可以通过遍历fdset，来找到就绪的描述符。</p>
<p>select目前几乎在所有的平台上支持，其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制，在Linux上一般为1024，可以通过修改宏定义甚至重新编译内核的方式提升这一限制，但是这样也会造成效率的降低。</p>
<p>内核需要传递消息到用户空间，需要内存拷贝</p>
</li>
<li>
<p>相对于select和poll来说，epoll更加灵活，没有描述符限制。epoll使用一个文件描述符管理多个描述符，将用户关系的文件描述符的事件存放到内核的一个事件表中，这样在用户空间和内核空间的copy只需一次。</p>
<p>epoll能打开的FD的上限远大于1024（1G的内存上能监听约10万个端口）。</p>
<p>效率提升，不是轮询的方式，不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数；即Epoll最大的优点就在于它只管你“活跃”的连接，而跟连接总数无关，因此在实际的网络环境中，Epoll的效率就会远远高于select和poll。</p>
</li>
</ol>
<h2 id="1-36-epoll有哪两种模式？">1.36 epoll有哪两种模式？</h2>
<p><strong>参考回答</strong></p>
<p><strong>epoll对文件描述符的操作有两种模式：<code>LT（level trigger）和ET（edge trigger）</code>。LT模式是默认模式，LT模式与ET模式的区别如下：</strong></p>
<p>LT模式：当epoll_wait检测到描述符事件发生并将此事件通知应用程序，<code>应用程序可以不立即处理该事件</code>。下次调用epoll_wait时，会再次响应应用程序并通知此事件。</p>
<p>ET模式：当epoll_wait检测到描述符事件发生并将此事件通知应用程序，<code>应用程序必须立即处理该事件</code>。如果不处理，下次调用epoll_wait时，不会再次响应应用程序并通知此事件。</p>
<ol>
<li>
<p><strong>LT模式</strong></p>
<p><code>LT(level triggered)是缺省的工作方式，并且同时支持block和no-block socket</code>。在这种做法中，内核告诉你一个文件描述符是否就绪了，然后你可以对这个就绪的fd进行IO操作。<code>如果你不作任何操作，内核还是会继续通知你的</code>。</p>
</li>
<li>
<p><strong>ET模式</strong></p>
<p><code>ET(edge-triggered)是高速工作方式，只支持no-block socket</code>。在这种模式下，当描述符从未就绪变为就绪时，内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪，并且不会再为那个文件描述符发送更多的就绪通知，直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如，你在发送，接收或者接收请求，或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误）。<code>但是请注意，如果一直不对这个fd作IO操作(从而导致它再次变成未就绪)，内核不会发送更多的通知(only once)</code>。</p>
<p><code>ET模式在很大程度上减少了epoll事件被重复触发的次数，因此效率要比LT模式高</code>。epoll工作在ET模式的时候，<code>必须使用非阻塞套接口</code>，以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。</p>
</li>
</ol>
<h2 id="1-37-说一下epoll的原理，它的查询速度是O-1-的吗？">1.37 说一下epoll的原理，它的查询速度是O(1)的吗？</h2>
<p><strong>参考回答</strong></p>
<p>epoll是一种更加高效的IO多路复用的方式，不同于忙轮询和无差别轮询，epoll会把哪个流发生了怎样的I/O事件通知我们。时间复杂度为O(1)。</p>
<p>epoll的执行过程如图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/epoll.jpeg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<ol>
<li>创建红黑树，调用epoll_create()创建一颗空的红黑树，用于存放FD及其感兴趣事件；</li>
<li>注册感兴趣事件，调用epoll_ctl()向红黑树中添加节点（FD及其感兴趣事件），时间复杂度O(logN)，向内核的中断处理程序注册一个回调函数，告诉内核，如果这个句柄的中断到了，就把它添加到就绪队列中。所以，当一个socket上有数据到了，内核在把网卡上的数据copy到内核中后就来把socket插入到就绪队列中了；</li>
<li>获取就绪事件，调用epoll_wait()返回就绪队列中的就绪事件，时间复杂度O(1)；</li>
</ol>
<h2 id="1-38-介绍域名解析成IP的全过程。">1.38 介绍域名解析成IP的全过程。</h2>
<p><strong>参考回答</strong></p>
<p><strong>第一步：检查浏览器缓存中是否缓存过该域名对应的IP地址</strong></p>
<p>用户通过浏览器浏览过某网站之后，浏览器就会自动缓存该网站域名对应的地址，当用户再次访问的时候，浏览器就会从缓存中查找该域名对应的IP地址，因为缓存不仅是有大小限制，而且还有时间限制（域名被缓存的时间通过属性来设置），所以存在域名对应的找不到的情况。当浏览器从缓存中找到了该网站域名对应的地址，那么整个解析过程结束，如果没有找到，将进行下一步骤。对于的缓存时间问题，不宜设置太长的缓存时间，时间太长，如果域名对应的发生变化，那么用户将在一段时间内无法正常访问到网站，如果太短，那么又造成频繁解析域名。</p>
<p><strong>第二步：如果在浏览器缓存中没有找到IP，那么将继续查找本机系统是否缓存过IP</strong></p>
<p>如果第一个步骤没有完成对域名的解析过程，那么浏览器会去系统缓存中查找系统是否缓存过这个域名对应的地址，也可以理解为系统自己也具备域名解析的基本能力。在系统中，可以通过设置文件来将域名手动绑定到某上，文件位置在。对于普通用户，并不推荐自己手动绑定域名和，对于开发者来说，通过绑定域名和，可以轻松切换环境，可以从测试环境切换到开发环境，方便开发和测试。在系统中，黑客常常修改他的电脑的文件，将用户常常访问的域名绑定到他指定的上，从而实现了本地解析，导致这些域名被劫持。在或者系统中，文件在，修改该文件也可以实现同样的目的。</p>
<p>前两步都是在本机上完成的，所以没有在上面示例图上展示出来，从第三步开始，才正在地向远程DNS服务器发起解析域名的请求。</p>
<p><strong>第三步：向本地域名解析服务系统发起域名解析的请求</strong></p>
<p>如果在本机上无法完成域名的解析，那么系统只能请求本地域名解析服务系统进行解析，本地域名系统一般都是本地区的域名服务器，比如你连接的校园网，那么域名解析系统就在你的校园机房里，如果你连接的是电信、移动或者联通的网络，那么本地域名解析服务器就在本地区，由各自的运营商来提供服务。对于本地服务器地址，系统使用命令就可以查看，在和系统下，直接使用命令来查看服务地址。一般都缓存了大部分的域名解析的结果，当然缓存时间也受域名失效时间控制，大部分的解析工作到这里就差不多已经结束了，负责了大部分的解析工作。</p>
<p><strong>第四步：向根域名解析服务器发起域名解析请求</strong></p>
<p>本地域名解析器还没有完成解析的话，那么本地域名解析服务器将向根域名服务器发起解析请求。</p>
<p><strong>第五步：根域名服务器返回gTLD域名解析服务器地址</strong></p>
<p>本地域名解析向根域名服务器发起解析请求，根域名服务器返回的是所查域的通用顶级域（）地址，常见的通用顶级域有、、、等。</p>
<p><strong>第六步：向gTLD服务器发起解析请求</strong></p>
<p>本地域名解析服务器向gTLD服务器发起请求。</p>
<p><strong>第七步：gTLD服务器接收请求并返回Name Server服务器</strong></p>
<p>服务器接收本地域名服务器发起的请求，并根据需要解析的域名，找到该域名对应的域名服务器，通常情况下，这个服务器就是你注册的域名服务器，那么你注册的域名的服务商的服务器将承担起域名解析的任务。</p>
<p><strong>第八步：Name Server服务器返回IP地址给本地服务器</strong></p>
<p>服务器查找域名对应的地址，将地址连同值返回给本地域名服务器。</p>
<p><strong>第九步：本地域名服务器缓存解析结果</strong></p>
<p>本地域名服务器缓存解析后的结果，缓存时间由时间来控制。</p>
<h2 id="1-39-如何在Linux上配置一个IP地址，如果给定端口号如何解析出域名？">1.39 如何在Linux上配置一个IP地址，如果给定端口号如何解析出域名？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>配置Linux系统的IP地址的方法，主要有以下三种：</li>
</ol>
<ul>
<li>
<p>ifconfig</p>
<p>ifconfig 命令主要是用来查看网卡的配置信息，因为用它来配置网卡的IP地址时，只会临时生效（Linux服务器重启后就会失效）</p>
</li>
<li>
<p>setup</p>
<p>setup 命令是 redhat 系列的linux系统（如CentOS）中专有的命令工具。可以使用 setup 命令，来对网络配置中的IP地址、子网掩码、默认网关、DNS服务器进行设置。而且，setup 网络配置工具设置的IP地址会永久生效。</p>
</li>
<li>
<p>修改网卡的配置文件</p>
<p>直接修改网卡的配置文件，设置方法有两种：</p>
<ul>
<li>自动获取动态IP地址</li>
<li>手工配置静态的IP地址</li>
</ul>
</li>
</ul>
<ol start="2">
<li>使用dig命令解析域名</li>
</ol>
<h2 id="1-40-解释一下IP地址、子网掩码、网关。">1.40 解释一下IP地址、子网掩码、网关。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<h3 id="IP地址">IP地址</h3>
</li>
</ol>
<p>IP地址有一个32位的连接地址,由4个8位字段组成,8位字段称为8位位组,每个8位位组之间用点号隔开，用于标识TCP/IP宿主机。每个IP地址都包含两部分:网络ID和主机ID,网络ID 标识在同一个物理网络上的所有宿主机,主机ID标识网络上的每一个宿主机,运行TCP/IP的每个计算机都需要唯一的IP地址。</p>
<p>Intenet委员会定义了五种地址类型以适应不同尺寸的网络。地址类型定义网络ID使用哪些位,它也定义了网络的可能数目和每个网络可能的宿主机数目．</p>
<ol start="2">
<li>
<h3 id="子网掩码-Subnet-Mask">子网掩码(Subnet Mask)</h3>
</li>
</ol>
<p>使用子网可以把单个大网分成多个物理网络,并用路由器把它们连接起来。子网掩码用于屏蔽IP地址的一部分,使得TCP/IP能够区别网络ID和宿主机ID。当TCP/IP宿主机要通信时,子网掩码用于判断一个宿主机是在本地网络还是在远程网络。</p>
<p>缺省的子网掩码用于不分成子网的TCP/IP网络,对应于网络ID的所有位都置为1,每个8位位组的十进制数是255,对应于宿主机ID的所有位都置为0。</p>
<p>用于子网掩码的位数决定可能的子网数目和每个子网的宿主机数目,子网掩码的位数越多,则子网越多,但是宿主机也较少。</p>
<p>例:假设A类地址子网数是14,则所需位数至少为4,用于子网的位为: 　　11111111, 11110000, 00000000, 00000000, 子网掩码为255.240.0.0,每个子网的宿主机数目为2^20-2=1,048, 574个。</p>
<ol start="3">
<li>
<h3 id="网关（Gateway）">网关（Gateway）</h3>
</li>
</ol>
<p>网关就是一个网络连接到另一个网络的“关口”。 按照不同的分类标准，网关也有很多种。TCP/IP协议里的网关是最常用的，在这里我们所讲的“网关”均指TCP/ IP协议下的网关。</p>
<p>网关实质上是一个网络通向其他网络的IP地址。比如有网络A和网络B，网络A的IP地址范围为“192.168.1.1~192. 168.1.254”，子网掩码为255.255.255.0；网络B的IP地址范围为“192.168.2.1~192. 168.2.254”，子网掩码为255.255.255.0。在没有路由器的情况下，两个网络之间是不能进行TCP/IP通信的，即使是两个网络连接在同一台交换机（或集线器）上，TCP/IP协议也会根据子网掩码（255.255.255.0）判定两个网络中的主机处在不同的网络里。而要实现这两个网络之间的通信，则必须通过网关。</p>
<p>如果网络A中的主机发现数据包的目的主机不在本地网络中，就把数据包转发给它自己的网关，再由网关转发给网络B的网关，网络B的网关再转发给网络B的某个主机。网络B向网络A转发数据包的过程也是如此。而要实现这两个网络之间的通信，则必须通过网关。如果网络A中的主机发现数据包的目的主机不在本地网络中，就把数据包转发给它自己的网关，再由网关转发给网络B的网关，网络B的网关再转发给网络B的某个主机。网络B向网络A转发数据包的过程也是如此 所以说，只有设置好网关的IP地址，TCP/IP协议才能实现不同网络之间的相互通信。那么这个IP地址是哪台机器的IP地址呢？网关的IP地址是具有路由功能的设备的IP地址，具有路由功能的设备有路由器、启用了路由协议的服务器（实质上相当于一台路由器）、代理服务器（也相当于一台路由器）。</p>
<h2 id="1-41-说说IP如何寻址？">1.41 说说IP如何寻址？</h2>
<p><strong>参考回答</strong></p>
<p>IP寻址包括本地网络寻址和非本地网络寻址两部分</p>
<ol>
<li>
<p>本地网络寻址</p>
<p>假设有2个主机，他们是属于同一个网段。主机A和主机B，首先主机A通过本机的hosts表或者wins系统或dns系统先将主机B的计算机名转换为IP地址，然后用自己的IP地址与子网掩码计算出自己所出的网段，比较目的主机B的ip地址与自己的子网掩码，发现与自己是出于相同的网段，于是在自己的ARP缓存中查找是否有主机B的mac地址，如果能找到就直接做数据链路层封装并且通过网卡将封装好的以太网帧发送有物理线路上去。</p>
<p>如果arp缓存中没有主机B的的mac地址，主机A将启动arp协议通过在本地网络上的arp广播来查询主机B的mac地址，获得主机B的mac地址厚写入arp缓存表，进行数据链路层的封装，发送数据。</p>
</li>
<li>
<p>非本地网络寻址</p>
<p>假设2个主机不是相同的网段，不同的数据链路层网络必须分配不同网段的IP地址并且由路由器将其连接起来。主机A通过本机的hosts表或wins系统或dns系统先主机B的计算机名转换为IP地址，然后用自己的IP地址与子网掩码计算出自己所处的网段，比较目的目的主机B的IP地址，发现与自己处于不同的网段。于是主机A将知道应该将次数据包发送给自己的缺省网关，即路由器的本地接口。</p>
<p>主机A在自己的ARP缓存中查找是否有缺省网关的MAC地址，如果能够找到就直接做数据链路层封装并通过网卡，将封装好的以太网数据帧发送到物理线路上去，如果arp缓存表中没有缺省网关的Mac地址，主机A将启动arp协议通过在本地网络上的arp广播来查询缺省网关的mac地址，获得缺省网关的mac地址后写入arp缓存表，进行数据链路层的封装，发送数据。</p>
<p>数据帧到达路由器的接受接口后首先解封装，变成IP数据包，对IP包进行处理，根据目的IP地址查找路由表，决定转发接口后做适应转发接口数据链路层协议帧的封装，并且发送到下一跳路由器，此过程继续直至到达目的的网络与目的主机。</p>
</li>
</ol>
<h2 id="1-42-操作系统的地址有几种，请具体说明。">1.42 操作系统的地址有几种，请具体说明。</h2>
<p><strong>参考回答</strong></p>
<p>操作系统有物理地址、逻辑地址、线性地址（也叫虚拟地址）三种地址</p>
<ol>
<li>
<h3 id="物理地址">物理地址</h3>
<p>在存储器里以字节为单位存储信息，为正确地存放或取得信息，每一个字节单元给以一个唯一的存储器地址，称为物理地址（Physical Address），又叫实际地址或绝对地址。</p>
<p>地址从0开始编号，顺序地每次加1，因此存储器的物理地址空间是呈线性增长的。它是用二进制数来表示的，是无符号整数，书写格式为十六进制数。它是出现在CPU外部地址总线上的寻址物理内存的地址信号，是地址变换的最终结果。用于内存芯片级的单元寻址，与处理器和CPU连接的地址总线相对应。</p>
</li>
<li>
<h3 id="逻辑地址">逻辑地址</h3>
<p>逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元（memory cell）、存储单元（storage element）、网络主机（network host）的地址。 逻辑地址往往不同于物理地址（physical address），通过地址翻译器（address translator）或映射函数可以把逻辑地址转化为物理地址。</p>
<p>在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。把用户程序中使用的地址称为相对地址即逻辑地址。逻辑地址由两个16位的地址分量构成，一个为段基值，另一个为偏移量。两个分量均为无符号数编码。</p>
</li>
<li>
<h3 id="线性地址">线性地址</h3>
<p>线性地址（Linear Address）是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址，然后加上基地址就是线性地址。</p>
<p>线性地址是一个32位无符号整数，可以用来表示高达4GB的地址，也就是，高达4294967296个内存单元。线性地址通常用十六进制数字表示，值的范围从0x00000000到0xffffffff）。程序代码会产生逻辑地址，通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制，那么线性地址可以再经过变换以产生一个物理地址。当采用4KB分页大小的时候，线性地址的高10位为页目录项在页目录表中的编号，中间10位为页表中的页号，其低12位则为偏移地址。如果是使用4MB分页机制，则高10位页号，低22位为偏移地址。如果没有启用分页机制，那么线性地址直接就是物理地址。</p>
</li>
</ol>
<h2 id="1-44-DNS用了哪些协议？">1.44 DNS用了哪些协议？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<h3 id="DNS在进行区域传输的时候使用TCP协议，其它时候则使用UDP协议；">DNS在进行区域传输的时候使用TCP协议，其它时候则使用UDP协议；</h3>
<p>DNS的规范规定了2种类型的DNS服务器，一个叫主DNS服务器，一个叫辅助DNS服务器。在一个区中主DNS服务器从自己本机的数据文件中读取该区的DNS数据信息，而辅助DNS服务器则从区的主DNS服务器中读取该区的DNS数据信息。当一个辅助DNS服务器启动时，它需要与主DNS服务器通信，并加载数据信息，这就叫做区传送（zone transfer）。</p>
</li>
<li>
<h3 id="为什么既使用TCP又使用UDP？">为什么既使用TCP又使用UDP？</h3>
<p>UDP报文的最大长度为512字节，而TCP则允许报文长度超过512字节。当DNS查询超过512字节时，协议的TC标志出现删除标志，这时则使用TCP发送。通常传统的UDP报文一般不会大于512字节。</p>
</li>
<li>
<h3 id="区域传送时使用TCP，主要有以下两点考虑：">区域传送时使用TCP，主要有以下两点考虑：</h3>
<ol>
<li>辅域名服务器会定时（一般时3小时）向主域名服务器进行查询以便了解数据是否有变动。如有变动，则会执行一次区域传送，进行数据同步。区域传送将使用TCP而不是UDP，因为数据同步传送的数据量比一个请求和应答的数据量要多得多。</li>
<li>TCP是一种可靠的连接，保证了数据的准确性。</li>
</ol>
</li>
<li>
<h3 id="域名解析时使用UDP协议：">域名解析时使用UDP协议：</h3>
<p>客户端向DNS服务器查询域名，一般返回的内容都不超过512字节，用UDP传输即可。不用经过TCP三次握手，这样DNS服务器负载更低，响应更快。虽然从理论上说，客户端也可以指定向DNS服务器查询的时候使用TCP，但事实上，很多DNS服务器进行配置的时候，仅支持UDP查询包。</p>
</li>
</ol>
<h2 id="1-45-说一说你对Linux内核的了解。">1.45 说一说你对Linux内核的了解。</h2>
<p><strong>参考回答</strong></p>
<p>内核是操作系统的核心，具有很多最基本功能，它负责管理系统的进程、内存、设备驱动程序、文件和网络系统，决定着系统的性能和稳定性。</p>
<p>Linux 内核有 4 项工作：</p>
<ol>
<li><strong>内存管理：</strong> 追踪记录有多少内存存储了什么以及存储在哪里</li>
<li><strong>进程管理：</strong> 确定哪些进程可以使用中央处理器（CPU）、何时使用以及持续多长时间</li>
<li><strong>设备驱动程序：</strong> 充当硬件与进程之间的调解程序/解释程序</li>
<li><strong>系统调用和安全防护：</strong> 从流程接受服务请求</li>
</ol>
<p>在正确实施的情况下，内核对于用户是不可见的，它在自己的小世界（称为内核空间）中工作，并从中分配内存和跟踪所有内容的存储位置。用户所看到的内容（例如 Web 浏览器和文件则被称为用户空间。这些应用通过系统调用接口（SCI）与内核进行交互。</p>
<p><em>举例来说，</em> 内核就像是一个为高管（硬件）服务的忙碌的个人助理。助理的工作就是将员工和公众（用户）的消息和请求（进程）转交给高管，记住存放的内容和位置（内存），并确定在任何特定的时间谁可以拜访高管、会面时间有多长。</p>
<p>为了更具象地理解内核，不妨将 Linux 计算机想象成有三层结构：</p>
<p>硬件：物理机（这是系统的底层结构或基础）是由内存（RAM）、处理器（或 CPU）以及输入/输出（I/O）设备（例如存储、网络和图形）组成的。其中，CPU 负责执行计算和内存的读写操作。</p>
<p>Linux 内核：操作系统的核心。它是驻留在内存中的软件，用于告诉 CPU 要执行哪些操作。</p>
<p>用户进程：这些是内核所管理的运行程序。用户进程共同构成了用户空间。用户进程有时也简称为进程。内核还允许这些进程和服务器彼此进行通信（称为进程间通信或 IPC）。</p>
<p>系统执行的代码通过以下两种模式之一在 CPU 上运行：内核模式或用户模式。在内核模式下运行的代码可以不受限制地访问硬件，而用户模式则会限制 SCI 对 CPU 和内存的访问。内存也存在类似的分隔情况（内核空间和用户空间）。这两个小细节构成了一些复杂操作的基础，例如安全防护、构建容器和虚拟机的权限分隔。</p>
<p>这也意味着：如果进程在用户模式下失败，则损失有限，无伤大雅，可以由内核进行修复。另一方面，由于内核进程要访问内存和处理器，因此内核进程的崩溃可能会引起整个系统的崩溃。由于用户进程之间会有适当的保护措施和权限要求，因此一个进程的崩溃通常不会引起太多问题。</p>
<h2 id="1-46-说一说你对Linux内核态与用户态的了解。">1.46 说一说你对Linux内核态与用户态的了解。</h2>
<p><strong>参考回答</strong></p>
<p><strong>内核态</strong>其实从本质上说就是内核，它是一种<strong>特殊的软件程序，控制计算机的硬件资源，例如协调CPU资源，分配内存资源，并且提供稳定的环境供应用程序运行</strong>。</p>
<p><strong>用户态</strong>就是提供应用程序运行的空间，为了使应用程序访问到内核管理的资源例如CPU，内存，I/O。内核必须提供一组通用的访问接口，这些接口就叫<strong>系统调用。</strong></p>
<ol>
<li><strong>系统调用</strong>是操作系统的最小功能单位。根据不同的应用场景，不同的Linux发行版本提供的系统调用数量也不尽相同，大致在240-350之间。这些系统调用组成了用户态跟内核态交互的基本接口。</li>
<li>从用户态到内核态切换可以通过三种方式：
<ol>
<li>系统调用：系统调用本身就是中断，但是是软件中断，跟硬中断不同。</li>
<li>异常：如果当前进程运行在用户态，如果这个时候发生了异常事件，就会触发切换。例如：缺页异常。</li>
<li>外设中断：当外设完成用户的请求时，会向CPU发送中断信号。</li>
</ol>
</li>
</ol>
<h2 id="1-49-谈谈Linux的内存管理。">1.49 谈谈Linux的内存管理。</h2>
<p><strong>参考回答</strong></p>
<p>常见的计算机存储层次如下：</p>
<ul>
<li>寄存器：CPU提供的，读写ns级别，容量字节级别。</li>
<li>CPU缓存：CPU和CPU间的缓存，读写10ns级别，容量较大一些，百到千节。</li>
<li>主存：动态内存，读写100ns级别，容量GB级别。</li>
<li>外部存储介质：磁盘、SSD，读写ms级别，容量可扩展到TB级别。</li>
</ul>
<p>CPU内的缓存示意图如下：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/s4.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>其中 L1d 和 L1i 都是CPU内部的cache，</p>
<ul>
<li>L1d 是数据cache。</li>
<li>L1i 是指令缓存。</li>
<li>L2是CPU内部的，不区分指令和数据的。</li>
<li>由于现代PC有多个CPU，L3缓存多个核心共用一个。</li>
</ul>
<p>对于编程人员来说，绝大部分观察主存和外部存储介质就可以了。如果要做极致的性能优化，可以关注L1、L2、L3的cache，比如nginx的绑核操作、pthread调度会影响CPU cache等。</p>
<p><strong>1. 虚拟内存</strong></p>
<p>物理内存是有限的（即使支持了热插拔）、非连续的，不同的CPU架构对物理内存的组织都不同。这使得直接使用物理内存非常复杂，为了降低使用内存的复杂度，引入了虚拟内存机制。</p>
<p>虚拟内存抽象了应用程序物理内存的细节，只允许物理内存保存所需的信息（按需分页），并提供了一种保护和控制进程间数据共享数据的机制。有了虚拟内存机制之后，每次访问可以使用更易理解的虚拟地址，让CPU转换成实际的物理地址访问内存，降低了直接使用、管理物理内存的门槛。</p>
<p>物理内存按大小被分成页框、页，每块物理内存可以被映射为一个或多个虚拟内存页。这块映射关系，由操作系统的页表来保存，页表是有层级的。层级最低的页表，保存实际页面的物理地址，较高层级的页表包含指向低层级页表的物理地址，指向顶级的页表的地址，驻留在寄存器中。当执行地址转换时，先从寄存器获取顶级页表地址，然后依次索引，找到具体页面的物理地址。</p>
<p><strong>2. 大页机制</strong></p>
<p>虚拟地址转换的过程中，需要好几个内存访问，由于内存访问相对CPU较慢，为了提高性能，CPU维护了一个TLB地址转换的cache，TLB是比较重要且珍稀的缓存，对于大内存工作集的应用程序，会因TLB命中率低大大影响到性能。</p>
<p>为了减少TLB的压力，增加TLB缓存的命中率，有些系统会把页的大小设为MB或者GB，这样页的数目少了，需要转换的页表项也小了，足以把虚拟地址和物理地址的映射关系，全部保存于TLB中。</p>
<p><strong>3. 区域概念</strong></p>
<p>通常硬件会对访问不同的物理内存的范围做出限制，在某些情况下设备无法对所有的内存区域做DMA。在其他情况下，物理内存的大小也会超过了虚拟内存的最大可寻址大小，需要执行特殊操作，才能访问这些区域。这些情况下，Linux对内存页的可能使用情况将其分组到各自的区域中（方便管理和限制）。比如ZONE_DMA用于指明哪些可以用于DMA的区域，ZONE_HIGHMEM包含未永久映射到内核地址空间的内存，ZONE_NORMAL标识正常的内存区域。</p>
<p><strong>4. 节点</strong></p>
<p>多核CPU的系统中，通常是NUMA系统（非统一内存访问系统）。在这种系统中，内存被安排成具有不同访问延迟的存储组，这取决于与处理器的距离。每一个库，被称为一个节点，每个节点Linux构建了一个独立的内存管理子系统。一个节点有自己的区域集、可用页和已用页表和各种统计计数器。</p>
<p><strong>5. page cache</strong></p>
<p>从外部存储介质中加载数据到内存中，这个过程是比较耗时的，因为外部存储介质读写性能毫秒级。为了减少外部存储设备的读写，Linux内核提供了Page cache。最常见的操作，每次读取文件时，数据都会被放入页面缓存中，以避免后续读取时所进行昂贵的磁盘访问。同样，当写入文件时，数据被重新放置在缓存中，被标记为脏页，定期的更新到存储设备上，以提高读写性能。</p>
<p><strong>6. 匿名内存</strong></p>
<p>匿名内存或者匿名映射表示不受文件系统支持的内存，比如程序的堆栈隐式创立的，或者显示通过mmap创立的。</p>
<p><strong>7. 内存回收</strong></p>
<p>贯穿系统的生命周期，一个物理页可存储不同类型的数据，可以是内核的数据结构，或是DMA访问的buffer，或是从文件系统读取的数据，或是用户程序分配的内存等。</p>
<p>根据页面的使用情况，Linux内存管理对其进行了不同的处理，可以随时释放的页面，称之为可回收页面，这类页面为：页面缓存或者是匿名内存（被再次交换到硬盘上）</p>
<p>大多数情况下，保存内部内核数据并用DMA缓冲区的页面是不能重新被回收的，但是某些情况下，可以回收使用内核数据结构的页面。例如：文件系统元数据的内存缓存，当系统处于内存压力情况下，可以从主存中丢弃它们。</p>
<p>释放可回收的物理内存页的过程，被称之为回收，可以同步或者异步的回收操作。当系统负载增加到一定程序时，kswapd守护进程会异步的扫描物理页，可回收的物理页被释放，并逐出备份到存储设备。</p>
<p><strong>8. compaction</strong></p>
<p>系统运行一段时间，内存就会变得支离破碎。虽然使用虚拟村内可以将分散的物理页显示为连续的物理页，但有时需要分配较大的物理连续内存区域。比如设备驱动程序需要一个用于DMA的大缓冲区时，或者大页内存机制分页时。内存compact可以解决了内存碎片的问题，这个机制将被占用的页面，从内存区域合适的移动，以换取大块的空闲物理页的过程，由kcompactd守护进程完成。</p>
<p><strong>9. OOM killer</strong></p>
<p>机器上的内存可能会被耗尽，并且内核将无法回收足够的内存用于运行新的程序，为了保存系统的其余部分，内核会调用OOM killer杀掉一些进程，以释放内存。</p>
<ol>
<li>段页机制</li>
</ol>
<p>段页机制是操作系统管理内存的一种方式，简单的来说，就是如何管理、组织系统中的内存。要理解这种机制，需要了解一下内存寻址的发展历程。</p>
<ul>
<li>直接寻址：早期的内存很小，通过硬编码的形式，直接定位到内存地址。这种方式有着明显的缺点：可控性弱、难以重定位、难以维护</li>
<li>分段机制：8086处理器，寻址空间达到1MB，即地址线扩展了20位，由于制作20位的寄存器较为困难，为了能在16位的寄存器的基础上，寻址20位的地址空间，引入了<code>段</code>的概念，即<code>内存地址=段基址左移4位+偏移</code></li>
<li>分页机制：随着寻址空间的进一步扩大、虚拟内存技术的引入，操作系统引入了分页机制。引入分页机制后，逻辑地址经过段机制转换得到的地址仅是中间地址，还需要通过页机制转换，才能得到实际的物理地址。<code>逻辑地址 --&gt;(分段机制) 线性地址 --&gt;(分页机制) 物理地址</code>。</li>
</ul>
<h2 id="1-50-谈谈内存映射文件。">1.50 谈谈内存映射文件。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li><strong>内存映射（mmap）</strong> 是一种内存映射文件的方法，即将一个文件或者其他对象映射到进程的地址空间，实现文件磁盘地址和应用程序进程虚拟地址空间中一段虚拟地址的一一映射关系。实现这样的映射关系后，进程就可以采用指针的方式读写操作这一段内存，而系统会自动回写藏页面到对应的文件磁盘上。应用程序处理映射部分如同访问主存。</li>
</ol>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/s5.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<ol>
<li>mmap内存映射原理</li>
</ol>
<p>（1）线程启动映射过程，并在虚拟地址空间中为映射创建虚拟映射区域。</p>
<p>先在用户空间调用库函数mmap，并在进程当前进程的虚拟地址空间中，寻找一段空闲的满足要求的连续虚拟地址作为内存虚拟映射区域，对此区域初始化并插入进程的虚拟地址区域链表或树中。</p>
<p>（2）系统在内核空间调用内核函数mmap，实现文件物理地址和进程虚拟地址之间的一一映射关系。</p>
<p>（3）进程发起堆这片映射空间的访问</p>
<p>进程读写操作访问虚拟地址，查询页表，发现这一段地址并不在内存的物理页面上，因为虽然建立了映射关系，但是还没有将文件从磁盘移到内存中。由此发生缺页中断，内核请求从磁盘调入页面。调页过程先在交换缓存空间（swap cache）中查找，若没有则通过nopage函数把缺失页从磁盘调入内存。之后进程会对其做读写操作，若写操作改变了页面内容，一段时间后系统会自动回写脏页面到磁盘中。(修改过的脏页面不会立即更新到文件中，可以调用msync来强制同步，写入文件)</p>
<ol>
<li>mmap和分页文件操作的区别</li>
</ol>
<p>区别在于分页文件操作在进程访存时是需要先查询页面缓存 <strong>(page cache)</strong> 的，若发生缺页中断，需要通过inode定位文件磁盘地址，先把缺失文件复制到page cache，再从page cache复制到内存中，才能进行访问。这样访存需要经过两次文件复制，写操作也是一样。总结来说，常规文件操作为了提高读写效率和保护磁盘，使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中，由于页缓存处在内核空间，不能被用户进程直接寻址，所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。<strong>但mmap的优势在于，把磁盘文件与进程虚拟地址做了映射，这样可以跳过page cache，只使用一次数据拷贝。</strong></p>
<h4 id="1-51-谈谈虚拟内存模型。">1.51 谈谈虚拟内存模型。</h4>
<p><strong>参考回答</strong></p>
<p>虚拟内存分成五大区，分别为<strong>栈区、堆区、全局区（静态区）、文字常量区（常量存储区）、程序代码区</strong>。五大区特性如下：</p>
<ol>
<li>栈区（stack）： 由编译器自动分配释放 ，存放函数的参数值，局部变量的值等。其操作方式类似于数据结构中的栈。</li>
<li>堆区（heap）： 一般由程序员分配释放， 若程序员不释放，程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事，分配方式倒是类似于链表。</li>
<li>全局区（静态区）（static）：全局变量和静态变量的存储是放在一块的，初始化的全局变量和静态变量在一块区域， 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。</li>
<li>文字常量区（<strong>常量存储区</strong>） ：<strong>常量字符串</strong>就是放在这里的。 程序结束后由系统释放。这是一块比较特殊的存储区，他们里面存放的是常量，不允许修改。</li>
<li>程序代码区：存放函数体的二进制代码。</li>
</ol>
<p><strong>答案解析</strong></p>
<p>以32位的操作系统为例，32位的操作系统每个进程对应的虚拟内存为4G（2的32次方），其中内核区1G，用户区3G。结构图如下：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E7%BB%93%E6%9E%84%E5%9B%BE.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="虚拟内存模型结构图">
<h2 id="1-52-什么是物理内存和虚拟内存，为什么要有虚拟内存？">1.52 什么是物理内存和虚拟内存，为什么要有虚拟内存？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>物理内存及虚拟内存定义</p>
<p><strong>物理内存</strong>是相对于虚拟内存而言的。物理内存指通过物理内存条而获得的内存空间，而<strong>虚拟内存</strong>则是指将硬盘的一块区域划分来作为内存。内存主要作用是在计算机运行时为操作系统和各种程序提供临时储存。</p>
</li>
<li>
<p><strong>为什么要有虚拟内存</strong></p>
<p>在早期的计算机中，要运行一个程序，会把这些程序全都装入内存，程序都是直接运行在内存上的，也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时，必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。 <strong>早期内存分配方法实例：</strong></p>
<p>某台计算机总的内存大小是 128M ，现在同时运行两个程序 A 和 B ， A 需占用内存 10M ， B 需占用内存 110 。计算机在给程序分配内存时会采取这样的方法：先将内存中的前 10M 分配给程序 A ，接着再从内存中剩余的 118M 中划分出 110M 分配给程序 B 。这种分配方法可以保证程序 A 和程序 B 都能运行，但是这种简单的内存分配策略问题很多。如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%97%A9%E6%9C%9F%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E6%88%AA%E5%9B%BE-163592190836932.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>早期内存分配方法</p>
<p><strong>早期的内存分配方法存在如下几个问题</strong>**（为什么要有虚拟内存的原因）**：</p>
<p><strong>问题 1</strong> ：<strong>进程地址空间不隔离</strong>。由于程序都是直接访问物理内存，所以恶意程序可以随意修改别的进程的内存数据，以达到破坏的目的。有些非恶意的，但是有 bug 的程序也可能不小心修改了其它程序的内存数据，就会导致其它程序的运行出现异常。这种情况对用户来说是无法容忍的，因为用户希望使用计算机的时候，其中一个任务失败了，至少不能影响其它的任务。</p>
<p><strong>问题 2</strong> ：<strong>内存使用效率低</strong>。在 A 和 B 都运行的情况下，如果用户又运行了程序 C，而程序 C 需要 20M 大小的内存才能运行，而此时系统只剩下 8M 的空间可供使用，所以此时系统必须在已运行的程序中选择一个将该程序的数据暂时拷贝到硬盘上，释放出部分空间来供程序 C 使用，然后再将程序 C 的数据全部装入内存中运行。可以想象得到，在这个过程中，有大量的数据在装入装出，导致效率十分低下。</p>
<p><strong>问题 3</strong> ：<strong>程序运行的地址不确定</strong>。当内存中的剩余空间可以满足程序 C 的要求后，操作系统会在剩余空间中随机分配一段连续的 20M 大小的空间给程序 C 使用，因为是随机分配的，所以程序运行的地址是不确定的。</p>
</li>
<li>
<p>虚拟内存的实现（可以在页式或段式内存管理的基础上实现）</p>
<p>（1）在装入程序时，不必将其全部装入到内存，而只需将当前要执行的部分页面或段装入到内存，就可让程序开始执行；</p>
<p>（2）在程序执行过程中，如果需执行的指令或访的数据尚未在内存（称为缺页或缺段)，则由处理器通知操作系线将相应的页面或段调入到内存，然后继续执订程序；</p>
<p>（3）另一方面，操作系统将内存中暂时不用的页面或段调出保存在外存上，从而腾出更多空困空间存放将要装入的程字以及将要调入的页画或段。</p>
<p>**虚拟技术基本特征：**大的用户空间（物理内存和外存相结合形成虚拟空间）、部分交换（调入和调出是对部分虚拟地址空间进行的）、不连续性（物理内存分配的不连续，虚拟地址空间使用的不连续）。</p>
</li>
</ol>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p><strong>分段</strong></p>
<p>为了解决早期内存分配方式带来的问题，人们想到了一种变通的方法，就是增加一个中间层，利用一种间接的地址访问方法访问物理内存。按照这种方法，程序中访问的内存地址不再是实际的物理内存地址，而是一个虚拟地址，然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样，只要操作系统处理好虚拟地址到物理内存地址的映射，就可以保证不同的程序最终访问的内存地址位于不同的区域，彼此没有重叠，就可以达到内存地址空间隔离的效果。</p>
<p>当创建一个进程时，操作系统会为该进程分配一个 4GB 大小的虚拟进程地址空间。之所以是 4GB ，是因为在 32 位的操作系统中，一个指针长度是 4 字节，而 4 字节指针的寻址能力是从 0x00000000<s>0xFFFFFFFF，最大值 0xFFFFFFFF 表示的即为 4GB 大小的容量。与虚拟地址空间相对的，还有一个物理地址空间，这个地址空间对应的是真实的物理内存。如果你的计算机上安装了 512M 大小的内存，那么这个物理地址空间表示的范围是 0x00000000</s>0x1FFFFFFF 。当操作系统做虚拟地址到物理地址映射时，只能映射到这一范围，操作系统也只会映射到这一范围。当进程创建时，每个进程都会有一个自己的 4GB 虚拟地址空间。要注意的是这个 4GB 的地址空间是“虚拟”的，并不是真实存在的，而且每个进程只能访问自己虚拟地址空间中的数据，无法访问别的进程中的数据，通过这种方法实现了进程间的地址隔离。那是不是这 4GB 的虚拟地址空间应用程序可以随意使用呢？很遗憾，在 Windows 系统下，这个虚拟地址空间被分成了 4 部分： NULL 指针区、用户区、 64KB 禁入区、内核区。</p>
<p>（1）<strong>NULL指针区</strong> （0x00000000~0x0000FFFF）: 如果进程中的一个线程试图操作这个分区中的数据，CPU就会引发非法访问。他的作用是，调用 malloc 等内存分配函数时，如果无法找到足够的内存空间，它将返回 NULL。而不进行安全性检查。它只是假设地址分配成功，并开始访问内存地址 0x00000000（NULL）。由于禁止访问内存的这个分区，因此会发生非法访问现象，并终止这个进程的运行。</p>
<p>（2）<strong>用户模式分区</strong> ( 0x00010000~0xBFFEFFFF)：这个分区中存放进程的私有地址空间。一个进程无法以任何方式访问另外一个进程驻留在这个分区中的数据 （相同 exe，通过 copy-on-write 来完成地址隔离）。（在windows中，所有 .exe 和动态链接库都载入到这一区域。系统同时会把该进程可以访问的所有内存映射文件映射到这一分区）。</p>
<p>（3）<strong>隔离区</strong> (0xBFFF0000~0xBFFFFFFF)：这个分区禁止进入。任何试图访问这个内存分区的操作都是违规的。微软保留这块分区的目的是为了简化操作系统的现实。</p>
<p>（4）<strong>内核区</strong> (0xC0000000~0xFFFFFFFF)：这个分区存放操作系统驻留的代码。线程调度、内存管理、文件系统支持、网络支持和所有设备驱动程序代码都在这个分区加载。这个分区被所有进程共享。</p>
<p>应用程序能使用的只是用户区而已，大约 2GB 左右 ( 最大可以调整到 3GB) 。内核区为 2GB ，内核区保存的是系统线程调度、内存管理、设备驱动等数据，这部分数据供所有的进程共享，但应用程序是不能直接访问的。</p>
<p>**人们之所以要创建一个虚拟地址空间，目的是为了解决进程地址空间隔离的问题。**但程序要想执行，必须运行在真实的内存上，所以，必须在虚拟地址与物理地址间建立一种映射关系。这样，通过映射机制，当程序访问虚拟地址空间上的某个地址值时，就相当于访问了物理地址空间中的另一个值。人们想到了一种分段(Sagmentation) 的方法，它的思想是在虚拟地址空间和物理地址空间之间做一一映射。比如说虚拟地址空间中某个 10M 大小的空间映射到物理地址空间中某个 10M 大小的空间。这种思想理解起来并不难，操作系统保证不同进程的地址空间被映射到物理地址空间中不同的区域上，这样每个进程最终访问到的。</p>
<p>物理地址空间都是彼此分开的。通过这种方式，就实现了进程间的地址隔离。还是以实例说明，假设有两个进程 A 和 B ，进程 A 所需内存大小为 10M ，其虚拟地址空间分布在 0x00000000 到 0x00A00000 ，进程 B 所需内存为 100M ，其虚拟地址空间分布为 0x00000000 到 0x06400000 。那么按照分段的映射方法，进程 A 在物理内存上映射区域为 0x00100000 到 0x00B00000 ，，进程 B 在物理内存上映射区域为0x00C00000 到 0x07000000 。于是进程 A 和进程 B 分别被映射到了不同的内存区间，彼此互不重叠，实现了地址隔离。从应用程序的角度看来，进程 A 的地址空间就是分布在 0x00000000 到 0x00A00000 ，在做开发时，开发人员只需访问这段区间上的地址即可。应用程序并不关心进程 A 究竟被映射到物理内存的那块区域上了，所以程序的运行地址也就是相当于说是确定的了。 下图显示的是分段方式的内存映射方法：</p>
</li>
</ol>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%97%A9%E6%9C%9F%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E6%88%AA%E5%9B%BE.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="早期内存分配截图">
<p>分段方式的内存映射方法</p>
<p>这种分段的映射方法虽然解决了上述中的问题1和问题3，但并没能解决问题2，即内存的使用效率问题。在分段的映射方法中，每次换入换出内存的都是整个程序， 这样会造成大量的磁盘访问操作，导致效率低下。所以这种映射方法还是稍显粗糙，粒度比较大。实际上，程序的运行有局部性特点，在某个时间段内，程序只是访问程序的一小部分数据，也就是说，程序的大部分数据在一个时间段内都不会被用到。基于这种情况，人们想到了粒度更小的内存分割和映射方法，这种方法就是分页 (Paging) 。</p>
<ol>
<li>
<p><strong>分页</strong></p>
<p>分页的基本方法是，将地址空间分成许多的页。每页的大小由 CPU 决定，然后由操作系统选择页的大小。目前 Inter 系列的 CPU 支持 4KB 或 4MB 的页大小，而 PC上目前都选择使用 4KB 。按这种选择， 4GB 虚拟地址空间共可以分成 1048576 页， 512M 的物理内存可以分为 131072 个页。显然虚拟空间的页数要比物理空间的页数多得多。</p>
<p>在分段的方法中，每次程序运行时总是把程序全部装入内存，而分页的方法则有所不同。分页的思想是程序运行时用到哪页就为哪页分配内存，没用到的页暂时保留在硬盘上。当用到这些页时再在物理地址空间中为这些页分配内存，然后建立虚拟地址空间中的页和刚分配的物理内存页间的映射。下面通过介绍一个可执行文件的装载过程来说明分页机制的实现方法。</p>
<p>一个可执行文件 (PE 文件 ) 其实就是一些编译链接好的数据和指令的集合，它也会被分成很多页，在 PE 文件执行的过程中，它往内存中装载的单位就是页。当一个 PE 文件被执行时，操作系统会先为该程序创建一个 4GB 的进程虚拟地址空间。前面介绍过，虚拟地址空间只是一个中间层而已，它的功能是利用一种映射机制将虚拟地址空间映射到物理地址空间，所以，创建 4GB 虚拟地址空间其实并不是要真的创建空间，只是要创建那种映射机制所需要的数据结构而已，这种数据结构就是页目和页表。</p>
<p>当创建完虚拟地址空间所需要的数据结构后，进程开始读取 PE 文件的第一页。在PE 文件的第一页包含了 PE 文件头和段表等信息，进程根据文件头和段表等信息，将 PE 文件中所有的段一一映射到虚拟地址空间中相应的页 (PE 文件中的段的长度都是页长的整数倍 ) 。这时 PE 文件的真正指令和数据还没有被装入内存中，操作系统只是据 PE 文件的头部等信息建立了 PE 文件和进程虚拟地址空间中页的映射关系而已。当 CPU 要访问程序中用到的某个虚拟地址时，当 CPU 发现该地址并没有相相关联的物理地址时， CPU 认为该虚拟地址所在的页面是个空页面， CPU 会认为这是个页错误 (Page Fault) ， CPU 也就知道了操作系统还未给该 PE 页面分配内存，CPU 会将控制权交还给操作系统。操作系统于是为该 PE 页面在物理空间中分配一个页面，然后再将这个物理页面与虚拟空间中的虚拟页面映射起来，然后将控制权再还给进程，进程从刚才发生页错误的位置重新开始执行。由于此时已为 PE 文件的那个页面分配了内存，所以就不会发生页错误了。随着程序的执行，页错误会不断地产生，操作系统也会为进程分配相应的物理页面来满足进程执行的需求。</p>
<p>分页方法的核心思想就是当可执行文件执行到第 x 页时，就为第 x 页分配一个内存页 y ，然后再将这个内存页添加到进程虚拟地址空间的映射表中 , 这个映射表就相当于一个 y=f(x) 函数。应用程序通过这个映射表就可以访问到 x 页关联的 y 页了。</p>
</li>
</ol>
<h2 id="1-53-内存和缓存有什么区别？">1.53 内存和缓存有什么区别？</h2>
<p><strong>参考回答</strong></p>
<p>内存和缓存是计算机不同的组成部件。</p>
<ol>
<li>
<p>内存特性</p>
<p>内存也被称作内存储器，其作用是用于暂时存放CPU的运算数据，以及与硬盘等外部存储交换的数据。只要计算机在运行中，CPU就会把需要进行运算的数据调到内存中进行运算，当运算完成后CPU再将结果传送出来，内存的运行也决定了计算机的稳定运行。</p>
</li>
<li>
<p>缓存特性</p>
<p>CPU芯片面积和成本的因素影响，决定了缓存都很小。现在一般的缓存不过几M，CPU缓存的运行频率极高，一般是和处理器同频运作，工作效率远远大于系统内存和硬盘。实际工作时，CPU往往需要重复读取读取同样的数据块，而缓存容量的增大，可以大幅度提升CPU内部读取数据的命中率，而不用再到内存或者硬盘上寻找，以此提高系统性能。</p>
</li>
</ol>
<h2 id="1-54-请你说说缓存溢出。">1.54 请你说说缓存溢出。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>缓存溢出及其危害</p>
<p><strong>缓存溢出</strong>是指输入到一个缓冲区或者数据保存区域的数据量超过了其容量，从而导致覆盖了其它区域数据的状况。</p>
<p>攻击者造成并利用这种状况使系统崩溃或者通过插入特制的代码来控制系统。被覆盖的区域可能存有其它程序的变量、参数、类似于返回地址或指向前一个栈帧的指针等程序控制流数据。缓冲区可以位于堆、栈或进程的数据段。这种错误可能产生如下后果：</p>
<p>（1）破坏程序的数据；</p>
<p>（2）改变程序的控制流，因此可能访问特权代码。</p>
<p>最终很有可能造成程序终止。当攻击者成功地攻击了一个系统之后，作为攻击的一部分，程序的控制流可能会跳转到攻击者选择的代码处，造成的结果是被攻击的进程可以执行任意的特权代码（比如通过判断输入是否和密码匹配来访问特权代码，如果存在缓冲区漏洞，非法输入导致存放“密码”的内存区被覆盖，从而使得“密码”被改写，因此判断为匹配进而获得了特权代码的访问权）</p>
<p>缓冲区溢出攻击是最普遍和最具危害性的计算机安全攻击类型之一。</p>
</li>
<li>
<p>如何预防缓存溢出</p>
<p>广义上分为<strong>两类</strong>：</p>
<p>（1）编译时防御系统，目的是强化系统以抵御潜伏于新程序中的恶意攻击</p>
<p>（2）运行时预防系统，目的是检测并终止现有程序中的恶意攻击</p>
</li>
</ol>
<h2 id="1-55-深拷贝和浅拷贝的区别是什么，它们各自的使用场景是什么？">1.55 深拷贝和浅拷贝的区别是什么，它们各自的使用场景是什么？</h2>
<p><strong>参考回答</strong></p>
<p><strong>浅拷贝</strong>只是对指针的拷贝，拷贝后两个指针指向同一个内存空间；<strong>深拷贝</strong>不断对指针进行拷贝，而且对指针指向的内容进行拷贝，经深拷贝后的指针是指向两个不同的地址空间。</p>
<ol>
<li>
<p>浅拷贝</p>
<p>对一个已知对象进行拷贝时，编译系统会自动调用一次构造函数（拷贝构造函数），如果用户未定义拷贝构造函数，则会调用默认拷贝构造函数，调用一次构造函数，调用两次析构函数，两个对象的指针成员所指内存相同，但是程序结束时该内存被释放了两次，会造成内存泄漏问题。</p>
</li>
<li>
<p>深拷贝</p>
<p>在对含有指针成员的对象进行拷贝时，必须要自己定义拷贝构造函数，使拷贝后的对象指针成员有自己的内存空间，即进行深拷贝，这样就避免了内存泄漏的发生，调用一次构造函数，一次自定义拷贝构造函数，两次析构函数。两个对象的指针成员所指内容不同。</p>
</li>
</ol>
<h2 id="1-56-说说IO模型。">1.56 说说IO模型。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>什么是IO</p>
<p>我们都知道unix世界里，一切皆文件。而文件是什么呢？文件就是一串二进制流而已。无论是socket，还是FIFO、管道、终端，对我们来说，一切都是文件，一切都是流。在信息交换的过程中，我们都是对这些流进行数据的收发操作简称为<strong>I/O操作</strong>(input and output)。往流中读出数据，系统调用read；写入数据，系统调用write。</p>
<p>计算机里有这么多的流，我怎么知道要操作哪个流呢？</p>
<p>做到这个的就是<strong>文件描述符</strong>，即通常所说的fd，一个fd就是一个整数，所以对这个整数的操作就是对这个文件（流）的操作。我们创建一个socket，通过系统调用会返回一个文件描述符，那么剩下对socket的操作就会转化为对这个描述符的操作。不能不说这又是一种分层和抽象的思想。</p>
</li>
<li>
<p>IO交互</p>
<p>对于一个输入操作来说，进程IO系统调用后，内核会先看缓冲区有没有相应的缓存数据，没有的话再到设备中读取，因为设备IO一般速度较慢，需要等待，内核缓冲区有数据则直接复制到进程空间。所以，对于一个网络输入操作通常包括<strong>两个不同阶段</strong>：</p>
<p>（1）等待网络数据到达网卡-&gt;读取到内核缓冲区</p>
<p>（2）从内核缓冲区复制数据-&gt;用户空间</p>
<p>IO有内存IO、网络IO和磁盘IO三种，通常我们所说的IO指的是<strong>网络IO</strong>和<strong>磁盘IO</strong>两者。</p>
</li>
<li>
<p>五大I/O模型</p>
<p>Linux有五大I/O模型，分别为<strong>阻塞IO、同步非阻塞IO、IO多路复用、信号驱动IO、异步IO</strong>。五种IO模型特性分别如下：</p>
<p>（1）阻塞IO（blocking IO）</p>
<p>最传统的一种IO模型，即在读写数据过程中会发生阻塞现象。</p>
<p>当用户线程发出IO请求之后，内核会去查看数据是否就绪，如果没有就绪就会等待数据就绪，而用户线程就会处于阻塞状态，用户线程交出CPU。当数据就绪之后，内核会将数据拷贝到用户线程，并返回结果给用户线程，用户线程才解除block状态。</p>
<p>典型的阻塞IO模型的例子为：</p>
<p>data = socket.read();</p>
<p>如果数据没有就绪，就会一直阻塞在read方法。</p>
</li>
</ol>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E9%98%BB%E5%A1%9EIO%E6%A8%A1%E5%9E%8B.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>阻塞I/O模型</p>
<p>（2）同步非阻塞IO（nonblocking IO)</p>
<p>当用户线程发起一个read操作后，并不需要等待，而是马上就得到了一个结果。如果结果是一个error时，它就知道数据还没有准备好，于是它可以再次发送read操作。一旦内核中的数据准备好了，并且又再次收到了用户线程的请求，那么它马上就将数据拷贝到了用户线程，然后返回。</p>
<p>所以事实上，在非阻塞IO模型中，用户线程需要不断地询问内核数据是否就绪，也就说非阻塞IO不会交出CPU，而会一直占用CPU。</p>
<p>典型的非阻塞IO模型一般如下：</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-keyword">while</span>(<span class="hljs-literal">true</span>)&#123;   data = socket.<span class="hljs-built_in">read</span>();  <span class="hljs-keyword">if</span>(data!= error)&#123;    处理数据    <span class="hljs-keyword">break</span>;  &#125;&#125;<br></code></pre></td></tr></table></figure>
<p>但是对于非阻塞IO就有一个非常严重的问题，在while循环中需要不断地去询问内核数据是否就绪，这样会导致CPU占用率非常高，因此一般情况下很少使用while循环这种方式来读取数据。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E9%9D%9E%E9%98%BB%E5%A1%9EIO%E6%A8%A1%E5%9E%8B.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>同步非阻塞I/O模型</p>
<p>（3）IO多路复用（IO multiplexing）</p>
<p>多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。</p>
<p>在多路复用IO模型中，会有一个线程不断去轮询多个socket的状态，只有当socket真正有读写事件时，才真正调用实际的IO读写操作。因为在多路复用IO模型中，只需要使用一个线程就可以管理多个socket，系统不需要建立新的进程或者线程，也不必维护这些线 程和 进程，并且只有在真正有socket读写事件进行时，才会使用IO资源，所以它大大减少了资源占用。</p>
<p>在Java NIO中，是通过selector.select()去查询每个通道是否有到达事件，如果没有事件，则一直阻塞在那里，因此这种方式会导致用户线程的阻塞。</p>
<p>也许有朋友会说，我可以采用多线程+ 阻塞IO 达到类似的效果，但是由于在多线程 + 阻塞IO 中，每个socket对应一个线程，这样会造成很大的资源占用，并且尤其是对于长连接来说，线程的资源一直不会释放，如果后面陆续有很多连接的话，就会造成性能上的瓶颈。</p>
<p>而多路复用IO模式，通过一个线程就可以管理多个socket，只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。因此，多路复用IO比较适合连接数比较多的情况。</p>
<p>另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中，不断地询问socket状态时通过用户线程去进行的，而在多路复用IO中，轮询每个socket状态是内核在进行的，这个效率要比用户线程要高的多。</p>
<p>**注意:**多路复用IO模型是通过轮询的方式来检测是否有事件到达，并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说，一旦事件响应体很大，那么就会导致后续的事件迟迟得不到处理，并且会影响新的事件轮询。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/IO%E5%A4%8D%E7%94%A8%E6%A8%A1%E5%9E%8B.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>I/O多路复用模型</p>
<p>（4）信号驱动IO（signal driven IO）</p>
<p>在信号驱动IO模型中，当用户线程发起一个IO请求操作，会给对应的socket注册一个信号函数，然后用户线程会继续执行，当内核数据就绪时会发送一个信号给用户线程，用户线程接收到信号之后，便在信号函数中调用IO读写操作来进行实际的IO请求操作。这个一般用于UDP中，对TCP套接口几乎是没用的，原因是该信号产生得过于频繁，并且该信号的出现并没有说明发生了什么事情。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E4%BF%A1%E5%8F%B7%E9%A9%B1%E5%8A%A8IO%E6%A8%A1%E5%9E%8B.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>信号驱动I/O模型</p>
<p>（5）异步IO（asynchronous IO）</p>
<p>异步IO模型才是最理想的IO模型，在异步IO模型中，当用户线程发起read操作之后，立刻就可以开始去做其它的事。而另一方面，从内核的角度，当它收到一个asynchronous read之后，它会立刻返回，说明read请求已经成功发起了，因此不会对用户线程产生任何阻塞。然后，内核会等待数据准备完成，再将数据拷贝到用户线程，当这一切都完成之后，内核会给用户线程发送一个信号，告诉它read操作完成了。也就说用户线程完全不需要关心实际的整个IO操作是如何进行的，只需要先发起一个请求，当接收内核返回的成功信号时表示IO操作已经完成，可以直接去使用数据了。</p>
<p>也就说在异步IO模型中，IO操作的两个阶段都不会阻塞用户线程，这两个阶段都是由内核自动完成，然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。这点是和信号驱动模型有所不同的，在信号驱动模型中，当用户线程接收到信号表示数据已经就绪，然后需要用户线程调用IO函数进行实际的读写操作；而在异步IO模型中，收到信号表示IO操作已经完成，不需要再在用户线程中调用IO函数进行实际的读写操作。</p>
<p>**注意:**异步IO是需要操作系统的底层支持，在Java 7中，提供了Asynchronous IO（简称AIO）。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E5%BC%82%E6%AD%A5IO%E6%A8%A1%E5%9E%8B.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>异步I/O模型</p>
<p>前四种IO模型实际上都属于同步IO，只有最后一种是真正的异步IO，因为无论是多路复用IO还是信号驱动模型，IO操作的第2个阶段都会引起用户线程阻塞，也就是内核进行数据拷贝的过程都会让用户线程阻塞。</p>
<h2 id="1-57-Linux中的软链接和硬链接有什么区别？">1.57 Linux中的软链接和硬链接有什么区别？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>inode概念</p>
<p>inode是文件系统中存储文件元信息的区域，中文叫节点索引，每个节点索引包含了文件的创建者，大小，日期等等。可以通过ls -i file 命令查看inode的值。</p>
</li>
<li>
<p>根据 Linux 系统存储文件的特点，链接的方式分为<strong>软链接和硬链接</strong>2 种</p>
<p><strong>软链接</strong>相当于建立了一个新的快捷方式文件，该文件有自己的名称和inode以及物理存储的文件数据，文件数据里记录着如何跳转的设置数据，访问该快捷文件会被重新定向到原始文件，删除原始文件，软链文件失效；<strong>硬链接</strong>相当于为当前文件名对应的文件再建立了一个文件别名，别名对应的inode以及物理数据都是一样的，一旦建立，我们甚至根本无法区分谁是原始文件的原始名称，删除文件的其中一个名称，文件不会丢失，除非把所有的名称都删除。</p>
<p>如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E8%BD%AF%E7%A1%AC%E8%BF%9E%E6%8E%A5.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>hard link(硬链) 和file 都指向同一个 inode，inode对应了一个实际物理存储的文件。soft link（软链） 对应一个新的inode， 新的inode对应一个新的物理存储文件，物理存储文件又指向了目标文件 file。</p>
</li>
<li>
<p><strong>软连接和硬链接的区别</strong></p>
<p>(1) <strong>软链接</strong>可以为文件和目录（哪怕是不存在的）创建链接；<strong>硬链接</strong>只能为文件创建链接。<br>
(2) <strong>软链接</strong>可以跨文件系统；<strong>硬链接</strong>必须是同一个文件系统<br>
(3) <strong>硬链接</strong>因为只是文件的一个别名，所以不重复占用内存；<strong>软链接</strong>因为只是一个访问文件的快捷方式文件，文件内只包含快捷指向信息，所以占用很小的内存。<br>
(4) <strong>软链接</strong>的文件权限和源文件可以不一样；<strong>硬链接</strong>文件权限肯定是一样的，因为他们本来就是一个文件的不同名称而已。</p>
</li>
<li>
<p>二者使用场景</p>
<p>一般比较重要的文件我们担心文件被误删除且传统复制备份方式占用double数量的空间会造成浪费，可以使用硬链做备份来解决；软链接一般被用来设置可执行文件的快捷方式的路径。</p>
</li>
</ol>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p>创建硬链接</p>
<figure class="highlight autoit"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs autoit">[root<span class="hljs-symbol">@localhost</span> ~]<span class="hljs-meta"># touch cangls</span><br>[root<span class="hljs-symbol">@localhost</span> ~]<span class="hljs-meta"># ln /root/cangls /tmp</span><br><span class="hljs-meta">#建立硬链接文件，目标文件没有写文件名，会和原名一致</span><br><span class="hljs-meta">#也就是/tmp/cangls 是硬链接文件</span><br></code></pre></td></tr></table></figure>
</li>
<li>
<p>创建软链接</p>
<figure class="highlight autoit"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs autoit">[root<span class="hljs-symbol">@localhost</span> ~]<span class="hljs-meta"># touch bols</span><br>[root<span class="hljs-symbol">@localhost</span> ~]<span class="hljs-meta"># In -s /root/bols /tmp</span><br><span class="hljs-meta">#建立软链接文件</span><br></code></pre></td></tr></table></figure>
<p>**注意：**软链接文件的源文件必须写成绝对路径，而不能写成相对路径（硬链接没有这样的要求）；否则软链接文件会报错。</p>
</li>
</ol>
<h2 id="1-58-说说缺页中断机制。">1.58 说说缺页中断机制。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>缺页中断</p>
<p>在请求分页系统中，可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。当所要访问的页面不在内存时，会产生一次<strong>缺页中断</strong>，此时操作系统会根据页表中的外存地址在外存中找到所缺的一页，将其调入内存。<br>
　缺页中断的处理流程如下：</p>
<p>A. 在内存中有空闲物理页面时，分配一物理页帧f，转第E步；</p>
<p>B. 依据页面置换算法选择被替换的物理页帧f，对应逻辑页q；</p>
<p>C. 如果q被修改过，则把它写回外存；</p>
<p>D. 修改q的页表项中驻留位置为0；</p>
<p>E. 将需要访问的页p装入到物理页f；</p>
<p>F. 修改p的页表项驻留位为1，物理页帧号为f；</p>
<p>G. 重新执行产生缺页的指令。</p>
</li>
<li>
<p><strong>缺页中断与一般的中断存在区别</strong></p>
<p><strong>（1）范围不同</strong></p>
</li>
</ol>
<p>一般中断只需要保护现场然后就直接跳到需及时处理的地方；</p>
<p>缺页中断除了保护现场之外，还要判断内存中是否有足够的空间存储所需的页或段，然后再把所需页调进来再使用。</p>
<p><strong>（2）结果不同</strong></p>
<p>一般中断在处理完之后返回时，执行下一条指令；</p>
<p>缺页中断返回时，执行产生中断的那一条指令。</p>
<p><strong>（3）次数不同</strong></p>
<p>一般中断只产生一次，发生中断指令后转入相应处理程序进行处理，恢复被中断程序现场;</p>
<p>在指令执行期间产生和处理缺页中断信号，一条指令在执行期间，可能产生多次缺页中断。</p>
<p><strong>答案解析</strong></p>
<p><strong>产生缺页中断的几种情况</strong>：</p>
<ol>
<li>当内存管理单元（MMU）中确实没有创建虚拟物理页映射关系，并且在该虚拟地址之后再没有当前进程的线性区（vma）的时候，这将杀掉该进程；</li>
<li>当MMU中确实没有创建虚拟页物理页映射关系，并且在该虚拟地址之后存在当前进程的线性区vma的时候，这很可能是缺页中断，并且可能是栈溢出导致的缺页中断；</li>
<li>当使用malloc/mmap等希望访问物理空间的库函数/系统调用后，由于linux并未真正给新创建的vma映射物理页，此时若先进行写操作，将和2产生缺页中断的情况一样；若先进行读操作虽然也会产生缺页异常，将被映射给默认的零页，等再进行写操作时，仍会产生缺页中断，这次必须分配1物理页了，进入写时复制的流程；</li>
<li>当使用fork等系统调用创建子进程时，子进程不论有无自己的vma，它的vma都有对于物理页的映射，但它们共同映射的这些物理页属性为只读，即linux并未给子进程真正分配物理页，当父子进程任何一方要写相应物理页时，导致缺页中断的写时复制。</li>
</ol>
<h2 id="1-59-软中断和硬中断有什么区别？">1.59 软中断和硬中断有什么区别？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>硬中断</p>
<p>由与系统相连的外设(比如网卡、硬盘)自动产生的。主要是用来通知操作系统外设状态的变化。比如当网卡收到数据包的时候，就会发出一个中断。我们通常所说的中断指的是硬中断(hardirq)。</p>
</li>
<li>
<p>软终端</p>
<p>为了满足实时系统的要求，中断处理应该是越快越好。Linux为了实现这个特点，当中断发生的时候，硬中断处理那些短时间就可以完成的工作，而将那些处理事件比较长的工作，放到中断之后来完成，也就是软中断(softirq)来完成。</p>
</li>
<li>
<p>中断嵌套</p>
<p>Linux下<strong>硬中断是可以嵌套的</strong>，但是没有优先级的概念，也就是说任何一个新的中断都可以打断正在执行的中断，但同种中断除外。<strong>软中断不能嵌套</strong>，但相同类型的软中断可以在不同CPU上并行执行。</p>
</li>
<li>
<p><strong>软中断与硬中断之间的区别</strong></p>
<p>（1）<strong>硬中断</strong>是由外部事件引起的因此具有随机性和突发性；<strong>软中断</strong>是执行中断指令产生的，无外面事件中断请求信号，因此软中断的发生不是随机的而是由程序安排好的；</p>
<p>（2）<strong>硬中断</strong>的中断号是由中断控制器提供的；<strong>软中断</strong>的中断号是由指令直接给出的，无需使用中断控制器。</p>
<p>（3）<strong>硬中断</strong>的中断响应周期，CPU需要发中断回合信号；<strong>软中断</strong>的中断响应周期，CPU不需要发中断回合信号。</p>
<p>（4）<strong>硬中断</strong>是可屏蔽的；<strong>软中断</strong>是不可屏蔽的。</p>
</li>
</ol>
<h1>2. 计算机网络</h1>
<h2 id="1-10-介绍一下tcp的三次握手。">1.10 介绍一下tcp的三次握手。</h2>
<p><strong>参考回答</strong></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="三次握手">
<ol>
<li>第一次握手：建立连接时，客户端发送syn包（syn=x）到服务器，并进入<strong>SYN_SENT</strong>状态，等待服务器确认；SYN：同步序列编号（Synchronize Sequence Numbers）。</li>
<li>第二次握手：服务器收到syn包，必须确认客户的SYN（ack=x+1），同时自己也发送一个SYN包（syn=y），即SYN+ACK包，此时服务器进入<strong>SYN_RECV</strong>状态；</li>
<li>第三次握手：客户端收到服务器的SYN+ACK包，向服务器发送确认包ACK(ack=y+1），此包发送完毕，客户端和服务器进入<strong>ESTABLISHED</strong>（TCP连接成功）状态，完成三次握手。</li>
</ol>
<h2 id="1-11-介绍一下tcp的四次挥手。">1.11 介绍一下tcp的四次挥手。</h2>
<p><strong>参考回答</strong></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="四次挥手">
<ol>
<li>客户端进程发出连接释放报文，并且停止发送数据。释放数据报文首部，FIN=1，其序列号为seq=u（等于前面已经传送过来的数据的最后一个字节的序号加1），此时，客户端进入FIN-WAIT-1（终止等待1）状态。 TCP规定，FIN报文段即使不携带数据，也要消耗一个序号。</li>
<li>服务器收到连接释放报文，发出确认报文，ACK=1，ack=u+1，并且带上自己的序列号seq=v，此时，服务端就进入了CLOSE-WAIT（关闭等待）状态。TCP服务器通知高层的应用进程，客户端向服务器的方向就释放了，这时候处于半关闭状态，即客户端已经没有数据要发送了，但是服务器若发送数据，客户端依然要接受。这个状态还要持续一段时间，也就是整个CLOSE-WAIT状态持续的时间。</li>
<li>客户端收到服务器的确认请求后，此时，客户端就进入FIN-WAIT-2（终止等待2）状态，等待服务器发送连接释放报文（在这之前还需要接受服务器发送的最后的数据）。</li>
<li>服务器将最后的数据发送完毕后，就向客户端发送连接释放报文，FIN=1，ack=u+1，由于在半关闭状态，服务器很可能又发送了一些数据，假定此时的序列号为seq=w，此时，服务器就进入了LAST-ACK（最后确认）状态，等待客户端的确认。</li>
<li>客户端收到服务器的连接释放报文后，必须发出确认，ACK=1，ack=w+1，而自己的序列号是seq=u+1，此时，客户端就进入了TIME-WAIT（时间等待）状态。注意此时TCP连接还没有释放，必须经过2∗∗MSL（最长报文段寿命）的时间后，当客户端撤销相应的TCB后，才进入CLOSED状态。</li>
<li>服务器只要收到了客户端发出的确认，立即进入CLOSED状态。同样，撤销TCB后，就结束了这次的TCP连接。可以看到，服务器结束TCP连接的时间要比客户端早一些。</li>
</ol>
<h2 id="1-12-为什么需要四次挥手？">1.12 为什么需要四次挥手？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>四次挥手示意图</p>
</li>
<li>
<p>四次挥手过程</p>
<p>（1）客户端向服务器发送FIN控制报文段（首部中的 FIN 比特被置位）；</p>
<p>（2）服务端收到FIN，回复ACK。服务器进入关闭等待状态，发送FIN;</p>
<p>（3）客户端收到FIN，给服务器回复ACK，客户端进入等待状态（进入“等待”，以确保服务器收到ACK真正关闭连接）;</p>
<p>（4）服务端收到ACK，链接关闭。</p>
</li>
<li>
<p><strong>四次挥手原因</strong></p>
<p>TCP协议是一种<strong>面向连接的、可靠的、基于字节流的</strong>运输层通信协议。TCP是<strong>全双工模式</strong>，这就意味着，当客户端发出FIN报文段时，只是表示客户端已经没有数据要发送了，客户端告诉服务器，它的数据已经全部发送完毕了；但是，这个时候客户端还是可以接受来自服务端的数据；当服务端返回ACK报文段时，表示它已经知道客户端没有数据发送了，但是服务端还是可以发送数据到客户端的；当服务端也发送了FIN报文段时，这个时候就表示服务端也没有数据要发送了，就会告诉客户端，我也没有数据要发送了，之后彼此就会愉快的中断这次TCP连接。</p>
<p>简单地说，前 2 次挥手用于关闭一个方向的数据通道，后两次挥手用于关闭另外一个方向的数据通道。</p>
</li>
</ol>
<h2 id="1-13-为什么要有最后一次ACK？">1.13 为什么要有最后一次ACK？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>三次握手示意图</p>
</li>
<li>
<p>四次挥手过程</p>
<p>（1）客户端发送一个SYN0给服务器（选择初始序列号，不携带任何数据）</p>
<p>（2）服务器收到SYN0，回复SYN1和ACK（服务器分配缓存，选择自己初始序列号）</p>
<p>（3）客户端收到SYN1、ACK，回复ACK（可以包含数据）</p>
</li>
<li>
<p><strong>为什么要有最后一次ACK</strong></p>
<p>客户端首先向服务器发送一个连接请求，但是可能这个连接请求走了远路，等了很长时间，服务器都没有收到，那么客户端可能会再次发送，此时服务器端收到并且回复SYN、ACK；在这个时候最先发送的那个连接请求到达服务器，那么服务器会回复一个SYN，ACK；但是客户端表示自己已经收到确认了，并不搭理这个回复，那么服务器可能陷入等待，如果这种情况多了，那么会导致服务器瘫痪，所以要发送第三个确认。</p>
</li>
</ol>
<h2 id="1-15-介绍一下tcp粘包、拆包的机制。">1.15 介绍一下tcp粘包、拆包的机制。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>TCP粘包和拆包问题</p>
<p>TCP是一个“流”协议，所谓流，就是<strong>没有界限的一长串二进制数据</strong>。TCP作为传输层协议并不了解上层业务数据的具体含义，它会根据TCP缓冲区的实际情况进行数据包的划分，所以在业务上认为是一个完整的包，可能会被TCP拆分成多个包进行发送，也有可能把多个小的包封装成一个大的数据包发送，这就是所谓的TCP粘包和拆包问题。</p>
</li>
<li>
<p>产生TCP粘包和拆包的原因</p>
<p>我们知道TCP是以流动的方式传输数据的，传输的<strong>最小单位</strong>为一个报文段（Segment）。TCP Header中有个Options标识位。常见的标识位为MSS（Maximum Segment Size）指的是，连接层每次传输的数据有个最大限制MTU（Maximum Transmission Unit），一般是1500bit，超过这个量要分成多个报文段，MSS则是这个最大限制减去TCP的header，光是要传输的数据的大小，一般为1460bit。换算成字节，也就是180多字节。</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs">TCP为提高性能，发送端会将需要发送的数据发送到缓冲区，等待缓冲区满了以后，再将缓冲中的数据发送到接收方。同理，接收方也有缓冲区这样的机制来接受数据。<br></code></pre></td></tr></table></figure>
<p>发生TCP粘包、拆包主要是以下原因：</p>
<figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs css">（<span class="hljs-number">1</span>）应用程序写入数据大于套接字缓冲区大小，会发生拆包；<br>（<span class="hljs-number">2</span>）应用程序写入数据小于套接字缓冲区大小，网卡将应用多次写入的数据发送到网络上，这将会发送粘包；<br>（<span class="hljs-number">3</span>）进行MSS（最大报文长度）大小的TCP分段，当TCP报文长度——TCP <span class="hljs-selector-tag">header</span>长度&gt;MSS  的时候会发生拆包；<br>（<span class="hljs-number">4</span>）接收方法不及时读取套接字缓冲区数据，这将发生粘包。<br></code></pre></td></tr></table></figure>
</li>
<li>
<p>如何处理粘包和拆包</p>
<p><strong>假设应用层协议是http</strong></p>
<p>我从浏览器中访问了一个网站，网站服务器给我发了200k的数据。建立连接的时候，通告的MSS是50k，所以为了防止ip层分片，tcp每次只会发送50k的数据，一共发了4个tcp数据包。如果我又访问了另一个网站，这个网站给我发了100k的数据，这次tcp会发出2个包，问题是，客户端收到6个包，怎么知道前4个包是一个页面，后两个是一个页面。既然是tcp将这些包分开了，那tcp会将这些包重组吗，它送给应用层的是什么？这是我自己想的一个场景，正式一点讲的话，<strong>这个现象叫拆包</strong>。</p>
<p>我们再考虑一个问题。</p>
<p>tcp中有一个negal算法，用途是这样的：通信两端有很多小的数据包要发送，虽然传送的数据很少，但是流程一点没少，也需要tcp的各种确认，校验。这样小的数据包如果很多，会造成网络资源很大的浪费，negal算法做了这样一件事，当来了一个很小的数据包，我不急于发送这个包，而是等来了更多的包，将这些小包组合成大包之后一并发送，不就提高了网络传输的效率的嘛。这个想法收到了很好的效果，但是我们想一下，如果是分属于两个不同页面的包，被合并在了一起，那客户那边如何区分它们呢？<strong>这就是粘包问题。</strong></p>
<p>从粘包问题我们更可以看出为什么tcp被称为流协议，因为它就跟水流一样，是没有边界的，没有消息的边界保护机制，所以<strong>tcp只有流的概念，没有包的概念</strong>。</p>
<p>我们还需要有两个概念：</p>
<p>（1）<strong>长连接</strong>： Client方与Server方先建立通讯连接，连接建立后不断开， 然后再进行报文发送和接收。<br>
​ （2）<strong>短连接</strong>：Client方与Server每进行一次报文收发交易时才进行通讯连接，交易完毕后立即断开连接。此种方式常用于一点对多点 通讯，比如多个Client连接一个Server。</p>
<p><strong>实际</strong>，我想象的<strong>关于粘包的场景是不对的</strong>，http连接是短连接，请求之后，收到回答，立马断开连接，不会出现粘包。 拆包现象是有可能存在的。</p>
<p><strong>处理拆包</strong>这里提供两种方法：</p>
<p>（1）通过包头+包长+包体的协议形式，当服务器端获取到指定的包长时才说明获取完整。<br>
​ （2） 指定包的结束标识，这样当我们获取到指定的标识时，说明包获取完整。</p>
<p><strong>处理粘包</strong>我们从上面的分析看到，虽然像http这样的短连接协议不会出现粘包的现象，但是一旦建立了长连接，粘包还是有可能会发生的。处理粘包的方法如下：</p>
<p>（1）发送方对于发送方造成的粘包问题，可以通过关闭Nagle算法来解决，使用TCP_NODELAY选项来关闭算法。</p>
<p>（2）接收方没有办法来处理粘包现象，只能将问题交给应用层来处理。应用层的解决办法简单可行，不仅能解决接收方的粘包问题，还可以解决发送方的粘包问题。解决办法：循环处理，应用程序从接收缓存中读取分组时，读完一条数据，就应该循环读取下一条数据，直到所有数据都被处理完成，判断每条数据的长度的方法有两种：</p>
<p>a. 格式化数据：每条数据有固定的格式（开始符，结束符），这种方法简单易行，但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。</p>
<p>b. 发送长度：发送每条数据时，将数据的长度一并发送，例如规定数据的前4位是数据的长度，应用层在处理时可以根据长度来判断每个分组的开始和结束位置。</p>
</li>
</ol>
<p><strong>答案解析</strong></p>
<p><strong>扩展资料</strong></p>
<p><strong>UDP会不会产生粘包问题呢？</strong></p>
<p>TCP为了保证可靠传输并减少额外的开销（每次发包都要验证），采用了基于流的传输，基于流的传输不认为消息是一条一条的，是无保护消息边界的（保护消息边界：指传输协议把数据当做一条独立的消息在网上传输，接收端一次只能接受一条独立的消息）。UDP则是面向消息传输的，是有保护消息边界的，接收方一次只接受一条独立的信息，所以不存在粘包问题。</p>
<p>举个例子：有三个数据包，大小分别为2k、4k、6k，如果采用UDP发送的话，不管接受方的接收缓存有多大，我们必须要进行至少三次以上的发送才能把数据包发送完，但是使用TCP协议发送的话，我们只需要接受方的接收缓存有12k的大小，就可以一次把这3个数据包全部发送完毕。</p>
<h2 id="1-16-介绍一下TCP和UDP的区别。">1.16 介绍一下TCP和UDP的区别。</h2>
<p><strong>参考回答</strong></p>
<p>TCP和UDP有如下区别：</p>
<ol>
<li>连接：TCP面向连接的传输层协议，即传输数据之前必须先建立好连接；UDP无连接。</li>
<li>服务对象：TCP点对点的两点间服务，即一条TCP连接只能有两个端点；UDP支持一对一，一对多，多对一，多对多的交互通信。</li>
<li>可靠性：TCP可靠交付：无差错，不丢失，不重复，按序到达；UDP尽最大努力交付，不保证可靠交付。</li>
<li>拥塞控制/流量控制：有拥塞控制和流量控制保证数据传输的安全性；UDP没有拥塞控制，网络拥塞不会影响源主机的发送效率。</li>
<li>报文长度：TCP动态报文长度，即TCP报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的；UDP面向报文，不合并，不拆分，保留上面传下来报文的边界。</li>
<li>首部开销：TCP首部开销大，首部20个字节；UDP首部开销小，8字节（源端口，目的端口，数据长度，校验和）。</li>
<li>适用场景（由特性决定）：数据完整性需让位于通信实时性，则应该选用TCP 协议（如文件传输、重要状态的更新等）；反之，则使用 UDP 协议（如视频传输、实时通信等）。</li>
</ol>
<h2 id="1-17-TCP和UDP对于网络稳定性有什么要求？">1.17 TCP和UDP对于网络稳定性有什么要求？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>TCP优缺点</p>
<p><strong>优点：可靠、稳定</strong></p>
<p>TCP的可靠体现在TCP在传输数据之前，会有三次握手来建立连接，而且在数据传递时，有确认、窗口、重传、拥塞控制机制，在数据传完之后，还会断开连接用来节约系统资源。</p>
<p><strong>缺点：慢，效率低，占用系统资源高，易被攻击</strong></p>
<p>在传递数据之前要先建立连接，这会消耗时间，而且在数据传递时，确认机制、重传机制、拥塞机制等都会消耗大量时间，而且要在每台设备上维护所有的传输连接。然而，每个链接都会占用系统的CPU、内存等硬件资源。因为TCP有确认机制、三次握手机制，这些也导致TCP容易被利用，实现DOS、DDOS、CC等攻击。</p>
</li>
<li>
<p>UDP优缺点</p>
<p><strong>优点：快，比TCP稍安全</strong></p>
<p>UDP没有TCP拥有的各种机制，是一个无状态的传输协议，所以传递数据非常快，没有TCP的这些机制，被攻击利用的机制就少一些，但是也无法避免被攻击。</p>
<p><strong>缺点：不可靠，不稳定</strong></p>
<p>因为没有TCP的那些机制，UDP在传输数据时，如果网络质量不好，就会很容易丢包，造成数据的缺失。</p>
</li>
<li>
<p>适用场景（<strong>网络稳定性要求</strong>）</p>
<p>TCP：当对网络通讯质量有要求时，比如HTTP、HTTPS、FTP等传输文件的协议， POP、SMTP等邮件传输的协议</p>
<p>UDP：对网络通讯质量要求不高时，要求网络通讯速度要快的场景。</p>
<p><strong>所以，TCP对网络稳定性要求高，而UDP相对弱一些。</strong></p>
</li>
</ol>
<h2 id="1-18-如何让UDP可靠一些？">1.18 如何让UDP可靠一些？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>为什么需要可靠的UDP</p>
<p>在弱网（2G、3G、信号不好）环境下，使用 TCP 连接的延迟很高，影响体验。使用 UDP 是很好的解决方案，既然把 UDP 作为弱网里面的 TCP 来使用，就必须保证数据传输能像 TCP 一样可靠</p>
</li>
<li>
<p>如何实现可靠的UDP</p>
<p>UDP它不属于连接型协议，因而具有资源消耗小，处理速度快的优点，所以通常音频、视频和普通数据在传送时使用UDP较多，因为它们即使偶尔丢失一两个数据包，也不会对接收结果产生太大影响。传输层无法保证数据的可靠传输，只能通过<strong>应用层</strong>来实现了。实现的方式可以参照tcp可靠性传输的方式，只是实现不在传输层，实现转移到了应用层。关键在于两点，从应用层角度考虑：</p>
<p>（1）提供超时重传，能避免数据报丢失。</p>
<p>（2）提供确认序列号，可以对数据报进行确认和排序。</p>
<p><strong>本端</strong>：首先在UDP数据报定义一个首部，首部包含确认序列号和时间戳，时间戳是用来计算RTT(数据报传输的往返时间)，计算出合适的RTO(重传的超时时间)。然后以等-停的方式发送数据报，即收到对端的确认之后才发送下一个的数据报。当时间超时，本端重传数据报，同时RTO扩大为原来的两倍，重新开始计时。</p>
<p><strong>对端</strong>：接受到一个数据报之后取下该数据报首部的时间戳和确认序列号，并添加本端的确认数据报首部之后发送给对端。根据此序列号对已收到的数据报进行排序并丢弃重复的数据报。</p>
</li>
</ol>
<p><strong>答案解析</strong></p>
<p><strong>扩展资料</strong></p>
<ol>
<li><strong>已经实现的可靠UDP：</strong></li>
</ol>
<p>（1）RUDP 可靠数据报传输协议；</p>
<p>（2）RTP 实时传输协议</p>
<p>为数据提供了具有实时特征的端对端传送服务；</p>
<p>Eg：组播或单播网络服务下的交互式视频、音频或模拟数据</p>
<p>（3）UDT</p>
<p>基于UDP的数据传输协议，是一种互联网传输协议；</p>
<p>主要目的是支持高速广域网上的海量数据传输，引入了新的拥塞控制和数据可靠性控制机制（互联网上的标准数据传输协议TCP在高带宽长距离的网络上性能很差）；</p>
<p>UDT是面向连接的双向的应用层协议，同时支持可靠的数据流传输和部分可靠的数据报服务；</p>
<p>应用：高速数据传输，点到点技术(P2P)，防火墙穿透，多媒体数据传输；</p>
<h2 id="1-19-TCP报文首部中序号占多少字节？">1.19 TCP报文首部中序号占多少字节？</h2>
<p><strong>参考回答</strong></p>
<p>序号字段占4个字节（32位）。</p>
<p><strong>答案解析</strong></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E9%A6%96%E9%83%A8%E5%AD%97%E6%AE%B5%E8%AF%A6%E7%BB%86%E5%9B%BE.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="首部字段详细图">
<p>TCP首部字段详细图</p>
<p>TCP首部包括20字节的固定首部部分及长度可变的其他选项，所以TCP首部长度可变。20个字节又分为5部分，每部分4个字节32位，如图中的5行，每行表示32位。</p>
<ol>
<li>
<p><strong>源端口和目的端口</strong>字段——各占 2 字节（16位）。端口是运输层与应用层的服务接口。运输层的复用和分用功能都要通过端口才能实现。</p>
</li>
<li>
<p><strong>序号</strong>字段——占 4 字节。TCP 连接中传送的数据流中的每一个字节都编上一个序号。序号字段的值则**指的是本报文段所发送的数据的第一个字节的序号。**比如分组的第一个数据包由文件的14个字节数据组成，那么该数据包所添加的序号就是1，同理第二个数据包由文件的59个字节数据组成，那么该数据包所添加的序号就是5；</p>
</li>
<li>
<p><strong>确认号</strong>字段——占 4 字节，是期望收到对方的下一个报文段的数据的第一个字节的序号。比如接收端收到由文件14个字节数据+TCP首部组成的数据包后，删除首部提取14个字节数据，返回的确认号为5，即告诉发送端下一次应该发送文件的第5个字节及其之后字节组成的数据包过来。</p>
</li>
<li>
<p><strong>数据偏移</strong>（即首部长度）——占 4 位，它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远，也就是TCP首部的长度。“数据偏移”的单位是 32 位字（以 4 字节为计算单位），最大1111表示15x4=60个字节，即表示TCP首部最大长度为60个字节，因此“选项”部分最多40个字节。</p>
</li>
<li>
<p><strong>保留</strong>字段——占 6 位，保留为今后使用，但目前应置为 0。</p>
</li>
<li>
<p>这里的六位二进制位，分别表示不同含义：</p>
<p>（1）<strong>紧急 URG</strong> —— 当 URG = 1 时，表明紧急指针字段有效。它告诉系统此报文段中有紧急数据，应尽快传送(相当于高优先级的数据)。 即URG=1的数据包不用排队直接优先传输。</p>
<p>（2）<strong>同步 SYN</strong> —— 同步 SYN = 1 表示这是一个连接请求或连接接受报文。即A想与B建立连接，发送过去的第一个数据包（第一次握手）中SYN=1；B返回的数据包（第二次握手）中SYN=1表示同意建立连接。</p>
<p>（3）<strong>确认 ACK</strong> —— 只有当 ACK = 1 时确认号字段才有效。当 ACK = 0 时，确认号无效。</p>
</li>
<li>
<p><strong>窗口</strong>字段 —— 占 2 字节，用来让对方设置发送窗口的依据，单位为字节。</p>
</li>
<li>
<p><strong>检验和</strong> —— 占 2 字节。检验和字段检验的范围包括首部和数据这两部分。在计算检验和时，要在 TCP 报文段的前面加上 12 字节的伪首部。</p>
</li>
<li>
<p><strong>紧急指针</strong>字段 —— 占 16 位，指出在本报文段中紧急数据共有多少个字节（紧急数据放在本报文段数据的最前面）。</p>
</li>
<li>
<p><strong>选项</strong>字段 —— 长度可变。TCP 最初只规定了一种选项，即最大报文段长度 MSS (Maximum Segment Size)是 TCP 报文段中的数据字段的最大长度。数据字段加上 TCP 首部才等于整个的 TCP 报文段。MSS 告诉对方 TCP：“我的缓存所能接收的报文段的数据字段的最大长度是 MSS 个字节。”<strong>其他选项</strong>有：窗口扩大选项、时间戳选项、选择确认选项（SACK）。</p>
</li>
<li>
<p><strong>填充</strong>字段 —— 这是为了使整个首部长度是 4 字节的整数倍。</p>
</li>
</ol>
<h2 id="1-20-TCP中的缓存有什么作用？">1.20 TCP中的缓存有什么作用？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>TCP缓冲区是什么</p>
<p>每个 socket 被创建后，都会分配两个缓冲区，输入缓冲区和输出缓冲区。</p>
</li>
<li>
<p><strong>缓冲区的意义（作用）</strong></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/TCP%E5%A5%97%E6%8E%A5%E5%AD%97%E7%9A%84IO%E7%BC%93%E5%86%B2%E5%8C%BA%E7%A4%BA%E6%84%8F%E5%9B%BE.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="TCP套接字的IO缓冲区示意图">
<p>TCP套接字的I/O缓冲区示意图</p>
<p><strong>TCP的发送缓冲区是用来缓存应用程序的数据，发送缓冲区的每个字节都有序列号，被应答确认的序列号对应的数据会从发送缓冲区删除掉。</strong></p>
<p>write()/send() 并不立即向网络中传输数据，而是先将数据写入缓冲区中，再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区，函数就可以成功返回，不管它们有没有到达目标机器，也不管它们何时被发送到网络，这些都是TCP协议负责的事情。<br>
​ TCP协议独立于 write()/send() 函数，数据有可能刚被写入缓冲区就发送到网络，也可能在缓冲区中不断积压，多次写入的数据被一次性发送到网络，比如nagle算法，这取决于当时的网络情况、当前线程是否空闲等诸多因素，不由程序员控制。<br>
​ read()/recv() 函数也是如此，也从输入缓冲区中读取数据，而不是直接从网络中读取。</p>
</li>
<li>
<p>I/O缓冲区特性</p>
<p>（1）I/O缓冲区在每个TCP套接字中单独存在；</p>
<p>（2）I/O缓冲区在创建套接字时自动生成；</p>
<p>（3）即使关闭套接字也会继续传送输出缓冲区中遗留的数据；</p>
<p>（4）关闭套接字将丢失输入缓冲区中的数据。</p>
<p>输入输出缓冲区的默认大小一般都是 8K，可以通过 getsockopt() 函数获取：</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs c++"><span class="hljs-comment">//代码实例（缓冲区大小获取）</span><br><span class="hljs-keyword">int</span> servSock = <span class="hljs-built_in">socket</span>(PF_INET, SOCK_STREAM, <span class="hljs-number">0</span>);<br><span class="hljs-keyword">unsigned</span> optVal;<br><span class="hljs-keyword">int</span> optLen = <span class="hljs-built_in"><span class="hljs-keyword">sizeof</span></span>(<span class="hljs-keyword">int</span>);<br><span class="hljs-built_in">getsockopt</span>(servSock, SOL_SOCKET, SO_SNDBUF, (<span class="hljs-keyword">char</span>*)&amp;optVal, &amp;optLen);<br><span class="hljs-comment">/*</span><br><span class="hljs-comment">    运行结果：</span><br><span class="hljs-comment">    Buffer length: 8192 </span><br><span class="hljs-comment">*/</span><br></code></pre></td></tr></table></figure>
</li>
</ol>
<h2 id="1-21-说一说TCP是怎么控制流量的？">1.21 说一说TCP是怎么控制流量的？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>所谓<strong>流量控制</strong>就是让发送发送速率不要过快，让接收方来得及接收。</p>
</li>
<li>
<p>TCP控制流量的方法</p>
<p>利用<strong>滑动窗口机制</strong>就可以实施流量控制。</p>
<p><strong>原理</strong>就是运用TCP报文段中的窗口大小字段来控制，发送方的发送窗口不可以大于接收方发回的窗口大小。考虑一种特殊的情况，就是接收方若没有缓存足够使用，就会发送零窗口大小的报文，此时发送放将发送窗口设置为0，停止发送数据。之后接收方有足够的缓存，发送了非零窗口大小的报文，但是这个报文在中途丢失的，那么发送方的发送窗口就一直为零导致死锁。</p>
<p>解决这个问题，TCP为每一个连接设置一个持续计时器（persistence timer）。只要TCP的一方收到对方的零窗口通知，就启动该计时器，周期性的发送一个零窗口探测报文段。对方就在确认这个报文的时候给出现在的窗口大小（注意：TCP规定，即使设置为零窗口，也必须接收以下几种报文段：零窗口探测报文段、确认报文段和携带紧急数据的报文段）。</p>
</li>
</ol>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p>TCP的滑动窗口</p>
<p>为了提高信道的利用率TCP协议不使用停止等待协议，而是使用连续ARQ协议，意思就是可以连续发出若干个分组然后等待确认，而不是发送一个分组就停止并等待该分组的确认。</p>
<p>TCP的两端都有发送/接收缓存和发送/接收窗口。TCP的缓存是一个循环队列，其中发送窗口可以用3个指针表示。而发送窗口的大小受TCP数据报中窗口大小的影响，TCP数据报中的窗口大小是接收端通知发送端其还可以接收多少数据，所以发送窗口根据接收的的窗口大小的值动态变化。</p>
<p>以下的几张图片就帮助理解一下滑动窗口的机制：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A31.PNG" srcset="/crystal575/img/loading.gif" lazyload class="" title="滑动窗口1">
<p>图1 根据B给出的窗口值，A构造出自己的发送窗口</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A32.PNG" srcset="/crystal575/img/loading.gif" lazyload class="" title="滑动窗口2">
<p>图2 A发送了11个字节的数据</p>
<p>注意上图中的3个指针P1、P2、P3！此时接收窗口中接收的数据可能是失序的，但是也先存储在接收缓存之中。发送确认号的时候依然发送31，表示B期望接收的下一个数据报的标示符是31。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A33.PNG" srcset="/crystal575/img/loading.gif" lazyload class="" title="滑动窗口3">
<p>图3 A收到新的确认号，发送窗口向前滑动</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A34.PNG" srcset="/crystal575/img/loading.gif" lazyload class="" title="滑动窗口4">
<p>图4 发送窗口内的序号都属于已经发送但未被确认</p>
<p>如果发送窗口中的数据报都属于已发送但未被确认的话，那么A就不能再继续发送数据，而需要进行等待。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A35.PNG" srcset="/crystal575/img/loading.gif" lazyload class="" title="滑动窗口5">
<p>图5 TCP的发送缓存和发送窗口（a）与接收缓存和接收窗口（b）</p>
</li>
<li>
<p>传输效率及Nagle算法</p>
<p>TCP的数据传输分为交互数据流和成块数据流，交互数据流一般是一些交互式应用程序的命令，所以这些数据很小，而考虑到TCP报头和IP报头的总和就有40字节，如果数据量很小的话，那么网络的利用效率就较低。</p>
<p>数据传输使用Nagle算法，Nagle算法很简单，就是规定一个TCP连接最多只能有一个<strong>未被确认的未完成的小分组</strong>。在该分组的确认到达之前不能发送其他的小分组。</p>
<p>但是也要考虑另一个问题，叫做糊涂窗口综合症。当接收方的缓存已满的时候，交互应用程序一次只从缓存中读取一个字节（这时候缓存中腾出一个字节），然后向发送方发送确认信息，此时发送方再发送一个字节（收到的窗口大小为1），这样网络的效率很低。</p>
<p>要解决这个问题，可以让接收方等待一段时间，使得接收缓存已有最够的空间容纳一个最长报文段，或者等到接收缓存已有一半的空间。只要这两种情况出现一种，就发送确认报文，同时发送方可以把数据积累成大的报文段发送。</p>
</li>
</ol>
<h2 id="1-22-HTTP2-0中TCP阻塞了怎么办？">1.22 HTTP2.0中TCP阻塞了怎么办？</h2>
<p><strong>参考回答</strong></p>
<p>HTTP2.0中TCP阻塞了有如下两种方法可以解决：</p>
<p><strong>（1）并发TCP连接</strong>（浏览器一个域名采用6-8个TCP连接，并发HTTP请求）<br>
​ <strong>（2）域名分片</strong>（多个域名，可以建立更多的TCP连接，从而提高HTTP请求的并发）</p>
<p><strong>答案解析</strong></p>
<p><strong>1. TCP队头阻塞</strong></p>
<p>TCP数据包是有序传输，中间一个数据包丢失，会等待该数据包重传，造成后面的数据包的阻塞。</p>
<p><strong>2. HTTP队头阻塞</strong></p>
<p>http队头阻塞和TCP队头阻塞完全不是一回事。</p>
<p>http1.x采用长连接(Connection:keep-alive)，可以在一个TCP请求上，发送多个http请求。</p>
<p>有非管道化和管道化，两种方式。</p>
<p><strong>非管道化</strong>，完全串行执行，请求-&gt;响应-&gt;请求-&gt;响应…，后一个请求必须在前一个响应之后发送。</p>
<p><strong>管道化</strong>，请求可以并行发出，但是响应必须串行返回。后一个响应必须在前一个响应之后。原因是，没有序号标明顺序，只能串行接收。</p>
<p><strong>管道化请求的致命弱点</strong>:</p>
<p>（1）会造成队头阻塞，前一个响应未及时返回，后面的响应被阻塞<br>
​ （2）请求必须是幂等请求，不能修改资源。因为，意外中断时候，客户端需要把未收到响应的请求重发，非幂等请求，会造成资源破坏。</p>
<p>由于这个原因，目前大部分浏览器和Web服务器，都关闭了管道化，采用非管道化模式。</p>
<p>无论是非管道化还是管道化，都会造成队头阻塞(请求阻塞)。</p>
<p><strong>解决http队头阻塞的方法：</strong></p>
<p><strong>（1）并发TCP连接</strong>（浏览器一个域名采用6-8个TCP连接，并发HTTP请求）<br>
​ <strong>（2）域名分片</strong>（多个域名，可以建立更多的TCP连接，从而提高HTTP请求的并发）</p>
<p><strong>2. HTTP2方式</strong></p>
<p>http2使用一个域名单一TCP连接发送请求，请求包被二进制分帧，不同请求可以互相穿插，避免了http层面的请求队头阻塞。<br>
​ 但是不能避免TCP层面的队头阻塞。</p>
<h2 id="1-23-TCP如何保证可靠性？">1.23 TCP如何保证可靠性？</h2>
<p><strong>参考回答</strong></p>
<p>TCP协议保证数据传输可靠性的方式主要有：<strong>校验和、序列号、确认应答、超时重传、连接管理、流量控制、拥塞控制</strong>。</p>
<ol>
<li>
<p>校验和</p>
<p>**计算方式：**在数据传输的过程中，将发送的数据段都当做一个16位的整数。将这些整数加起来。并且前面的进位不能丢弃，补在后面，最后取反，得到校验和。<br>
**发送方：**在发送数据之前计算检验和，并进行校验和的填充。<br>
**接收方：**收到数据后，对数据以同样的方式进行计算，求出校验和，与发送方的进行比对。</p>
<p>**<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%A0%A1%E9%AA%8C%E5%92%8C.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="校验和"></p>
<p>**注意：**如果接收方比对校验和与发送方不一致，那么数据一定传输有误。但是如果接收方比对校验和与发送方一致，<strong>数据不一定传输成功。</strong></p>
</li>
<li>
<p>序列号和确认应答</p>
<p>**序列号：**TCP传输时将每个字节的数据都进行了编号，这就是序列号。<br>
**确认应答：**TCP传输的过程中，每次接收方收到数据后，都会对传输方进行确认应答。也就是发送ACK报文。这个ACK报文当中带有对应的确认序列号，告诉发送方，接收到了哪些数据，下一次的数据从哪里发。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E7%A1%AE%E8%AE%A4.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="确认">
<p>序列号的作用不仅仅是应答的作用，有了序列号能够将接收到的数据根据序列号排序，并且去掉重复序列号的数据。这也是TCP传输可靠性的保证之一。</p>
</li>
<li>
<p>超时重传</p>
<p>在进行TCP传输时，由于确认应答与序列号机制，也就是说发送方发送一部分数据后，都会等待接收方发送的ACK报文，并解析ACK报文，判断数据是否传输成功。如果发送方发送完数据后，迟迟没有等到接收方的ACK报文，这该怎么办呢？而没有收到ACK报文的原因可能是什么呢？</p>
<p>首先，发送方没有接收到响应的ACK报文原因可能有两点：</p>
<p>（1）数据在传输过程中由于网络原因等直接全体丢包，接收方根本没有接收到。</p>
<p>（2）接收方接收到了响应的数据，但是发送的ACK报文响应却由于网络原因丢包了。</p>
<p>TCP在解决这个问题的时候引入了一个新的机制，叫做超时重传机制。**简单理解就是发送方在发送完数据后等待一个时间，时间到达没有接收到ACK报文，那么对刚才发送的数据进行重新发送。**如果是刚才第一个原因，接收方收到二次重发的数据后，便进行ACK应答。如果是第二个原因，接收方发现接收的数据已存在（判断存在的根据就是序列号，所以上面说序列号还有去除重复数据的作用），那么直接丢弃，仍旧发送ACK应答。</p>
</li>
<li>
<p>连接管理</p>
<p>连接管理就是三次握手与四次挥手的过程，保证可靠的连接，是保证可靠性的前提。</p>
</li>
<li>
<p>流量控制</p>
<p>收端在接收到数据后，对其进行处理。如果发送端的发送速度太快，导致接收端的结束缓冲区很快的填充满了。此时如果发送端仍旧发送数据，那么接下来发送的数据都会丢包，继而导致丢包的一系列连锁反应，超时重传呀什么的。而TCP根据接收端对数据的处理能力，决定发送端的发送速度，这个机制就是流量控制。</p>
<p>在TCP协议的报头信息当中，有一个16位字段的窗口大小。在介绍这个窗口大小时我们知道，窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大，证明接收端接收缓冲区的剩余空间越大，网络的吞吐量越大。接收端会在确认应答发送ACK报文时，将自己的即时窗口大小填入，并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小的值为0，那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段，让接收端把窗口大小告诉发送端。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="流量控制">
</li>
<li>
<p>拥塞控制</p>
<p>TCP传输的过程中，发送端开始发送数据的时候，如果刚开始就发送大量的数据，那么就可能造成一些问题。网络可能在开始的时候就很拥堵，如果给网络中在扔出大量数据，那么这个拥堵就会加剧。拥堵的加剧就会产生大量的丢包，就对大量的超时重传，严重影响传输。</p>
<p>所以TCP引入了慢启动的机制，在开始发送数据时，先发送少量的数据探路。探清当前的网络状态如何，再决定多大的速度进行传输。这时候就引入一个叫做拥塞窗口的概念。发送刚开始定义拥塞窗口为 1，每次收到ACK应答，拥塞窗口加 1。在发送数据之前，首先将拥塞窗口与接收端反馈的窗口大小比对，取较小的值作为实际发送的窗口。</p>
<p>拥塞窗口的增长是指数级别的。慢启动的机制只是说明在开始的时候发送的少，发送的慢，但是增长的速度是非常快的。为了控制拥塞窗口的增长，不能使拥塞窗口单纯的加倍，设置一个拥塞窗口的阈值，当拥塞窗口大小超过阈值时，不能再按照指数来增长，而是线性的增长。在慢启动开始的时候，慢启动的阈值等于窗口的最大值，一旦造成网络拥塞，发生超时重传时，慢启动的阈值会为原来的一半（这里的原来指的是发生网络拥塞时拥塞窗口的大小），同时拥塞窗口重置为 1。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="拥塞控制">
<p>拥塞控制是TCP在传输时尽可能快的将数据传输，并且避免拥塞造成的一系列问题。是可靠性的保证，同时也是维护了传输的高效性。</p>
</li>
</ol>
<h2 id="1-27-说一说你对IP分类的了解。">1.27 说一说你对IP分类的了解。</h2>
<p><strong>参考回答</strong></p>
 <img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/ip%E5%88%86%E7%B1%BB.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="ip分类">
<p>五类互联网地址</p>
<p>IP地址根据网络号和主机号来分，分为A、B、C三类及特殊地址D、E。 全0和全1的都保留不用。</p>
<ol>
<li>A类：(1.0.0.0-126.0.0.0)（默认子网掩码：255.0.0.0或 0xFF000000）第一个字节为网络号，后三个字节为主机号。该类IP地址的最前面为“0”，所以地址的网络号取值于1~126之间。一般用于大型网络。</li>
<li>B类：(128.0.0.0-191.255.0.0)（默认子网掩码：255.255.0.0或0xFFFF0000）前两个字节为网络号，后两个字节为主机号。该类IP地址的最前面为“10”，所以地址的网络号取值于128~191之间。一般用于中等规模网络。</li>
<li>C类：(192.0.0.0-223.255.255.0)（子网掩码：255.255.255.0或 0xFFFFFF00）前三个字节为网络号，最后一个字节为主机号。该类IP地址的最前面为“110”，所以地址的网络号取值于192~223之间。一般用于小型网络。</li>
<li>D类：是多播地址。该类IP地址的最前面为“1110”，所以地址的网络号取值于224~239之间。一般用于多路广播用户 。</li>
<li>E类：是保留地址。该类IP地址的最前面为“1111”，所以地址的网络号取值于240~255之间。</li>
</ol>
<h2 id="1-28-IP为什么要分类？">1.28 IP为什么要分类？</h2>
<p><strong>参考回答</strong></p>
<p>根据IP地址访问终端是通过路由器，路由设备当中有一张路由表，该路由表记录了所有IP地址的位置，这样就可以进行包的转发了，如果我们不区分网络地址，那么这张路由表当中就要保存有所有IP地址的方向，这张路由表就会很大，就像下面说的那样：如果不分网络位和主机位，路由器的路由表就是都是32位的地址，那所有的路由器维护的路由表会很大，转发速度会变慢（因为查询变慢）。而且所有的路由器都要有全Internet的地址，所有人的路由器都要有足够的性能来存下全网地址。估计建造这样的Internet成本是现在的几万倍，甚至更高。</p>
<p><strong>有了网络地址，就可以限定拥有相同网络地址的终端都在同一个范围内，那么路由表只需要维护这个网络地址的方向，就可以找到相应的终端了。</strong></p>
<h2 id="1-29-IPV4和IPV6有什么区别？">1.29 IPV4和IPV6有什么区别？</h2>
<p><strong>参考回答</strong></p>
<p>IPv4和IPv6是是目前使用的两种Internet协议版本，IPv4和IPv6协议之间存在各种差异，包括它们的功能，但关键的一点是它生成的地址（地址空间）的数量的区别。</p>
<ol>
<li>
<p>协议地址的区别</p>
<p>（1）地址长度</p>
<p>IPv4协议具有32位（4字节）地址长度；IPv6协议具有128位（16字节）地址长度。</p>
<p>（2）地址的表示方法</p>
<p>IPv4地址是以小数表示的二进制数。 IPv6地址是以十六进制表示的二进制数。</p>
<p>（3）地址配置</p>
<p>IPv4协议的地址可以通过手动或DHCP配置的。</p>
<p>IPv4协议需要使用Internet控制消息协议版本6（ICMPv6）或DHCPv6的无状态地址自动配置（SLAAC）。</p>
</li>
<li>
<p>数据包的区别</p>
<p>（1）包的大小</p>
<p>IPv4协议的数据包需要576个字节，碎片可选 ；IPv6协议的数据包需要1280个字节，不会碎片。</p>
<p>（2）包头</p>
<p>IPv4协议的包头的长度为20个字节，不识别用于QoS处理的数据包流，包含checksum，包含最多40个字节的选项字段。</p>
<p>IPv6协议的包头的长度为40个字节，包含指定QoS处理的数据包流的Flow Label字段，不包含checksum；IPv6协议没有字段，但IPv6扩展标头可用。</p>
<p>（3）数据包碎片</p>
<p>IPv4协议的数据包碎片会由转发路由器和发送主机完成。IPv6协议的数据包碎片仅由发送主机完成。</p>
</li>
<li>
<p>DNS记录</p>
<p>IPv4协议的地址（A）记录，映射主机名；指针（PTR）记录，IN-ADDR.ARPA DNS域。</p>
<p>IPv6协议的地址（AAAA）记录，映射主机名；指针（PTR）记录，IP6.ARPA DNS域</p>
</li>
<li>
<p>IPSec支持</p>
<p>IPv4协议的IPSec支持只是可选的；IPv6协议有内置的IPSec支持。</p>
</li>
<li>
<p>地址解析协议</p>
<p>IPv4协议：地址解析协议（ARP）可用于将IPv4地址映射到MAC地址。</p>
<p>IPv6协议：地址解析协议（ARP）被邻居发现协议（NDP）的功能所取代。</p>
</li>
<li>
<p>身份验证和加密</p>
<p>Pv6提供身份验证和加密；但IPv4不提供。</p>
</li>
</ol>
<h2 id="1-30-说一下http和https的区别。">1.30 说一下http和https的区别。</h2>
<p><strong>参考回答</strong></p>
<p>https和https主要存在以下的区别：</p>
<ol>
<li>HTTPS 协议需要到 CA （Certificate Authority，证书颁发机构）申请证书，一般免费证书较少，因而需要一定费用。(以前的网易官网是http，而网易邮箱是 https 。)</li>
<li>HTTP 是超文本传输协议，信息是明文传输，HTTPS 则是具有安全性的 SSL 加密传输协议。</li>
<li>HTTP 和 HTTPS 使用的是完全不同的连接方式，用的端口也不一样，前者是80，后者是443。</li>
<li>HTTP 的连接很简单，是无状态的。HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议，比 HTTP 协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)</li>
</ol>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p><strong>超文本传输协议</strong>（HTTP，HyperText Transfer Protocol）是互联网上应用最为广泛的一种网络协议。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。它可以使浏览器更加高效。HTTP 协议是以明文方式发送信息的，如果黑客截取了 Web 浏览器和服务器之间的传输报文，就可以直接获得其中的信息。</p>
</li>
<li>
<p><strong>HTTP原理</strong></p>
<p>（1）客户端的浏览器首先要通过网络与服务器建立连接，该连接是通过 TCP 来完成的，一般 TCP 连接的端口号是80。 建立连接后，客户机发送一个请求给服务器，请求方式的格式为：统一资源标识符（URL）、协议版本号，后边是 MIME 信息包括请求修饰符、客户机信息和许可内容。</p>
<p>（2）服务器接到请求后，给予相应的响应信息，其格式为一个状态行，包括信息的协议版本号、一个成功或错误的代码，后边是 MIME 信息包括服务器信息、实体信息和可能的内容。</p>
</li>
<li>
<p><strong>HTTPS</strong>（Hyper Text Transfer Protocol over SecureSocket Layer）是以安全为目标的 HTTP 通道，是 HTTP 的安全版。HTTPS 的安全基础是 SSL。SSL 协议位于 TCP/IP 协议与各种应用层协议之间，为数据通讯提供安全支持。SSL 协议可分为两层：SSL 记录协议（SSL Record Protocol），它建立在可靠的传输协议（如TCP）之上，为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL 握手协议（SSL Handshake Protocol），它建立在 SSL 记录协议之上，用于在实际的数据传输开始前，通讯双方进行身份认证、协商加密算法、交换加密密钥等。</p>
</li>
<li>
<p><strong>HTTPS的工作原理</strong></p>
<p>我们都知道HTTPS能够加密信息，以免敏感信息被第三方获取，所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/HTTPS%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86.gif" srcset="/crystal575/img/loading.gif" lazyload class="" title="HTTPS工作原理">
<p>客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤，如图上图所示：</p>
<p>（1）客户使用https的URL访问Web服务器，要求与Web服务器建立SSL连接。</p>
<p>（2）Web服务器收到客户端请求后，会将网站的证书信息（证书中包含公钥）传送一份给客户端。</p>
<p>（3）客户端的浏览器与Web服务器开始协商SSL连接的安全等级，也就是信息加密的等级。</p>
<p>（4）客户端的浏览器根据双方同意的安全等级，建立会话密钥，然后利用网站的公钥将会话密钥加密，并传送给网站。</p>
<p>（5）Web服务器利用自己的私钥解密出会话密钥。</p>
<p>（6）Web服务器利用会话密钥加密与客户端之间的通信。</p>
</li>
<li>
<p><strong>HTTPS的优点</strong></p>
<p>尽管HTTPS并非绝对安全，掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击，但HTTPS仍是现行架构下最安全的解决方案，主要有以下几个好处：</p>
<p>（1）使用HTTPS协议可认证用户和服务器，确保数据发送到正确的客户机和服务器；</p>
<p>（2）HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议，要比http协议安全，可防止数据在传输过程中不被窃取、改变，确保数据的完整性。</p>
<p>（3）HTTPS是现行架构下最安全的解决方案，虽然不是绝对安全，但它大幅增加了中间人攻击的成本。</p>
<p>（4）谷歌曾在2014年8月份调整搜索引擎算法，并称“比起同等HTTP网站，采用HTTPS加密的网站在搜索结果中的排名将会更高”。</p>
</li>
<li>
<p><strong>HTTPS的缺点</strong></p>
<p>虽然说HTTPS有很大的优势，但其相对来说，还是存在不足之处的：</p>
<p>（1）HTTPS协议握手阶段比较费时，会使页面的加载时间延长近50%，增加10%到20%的耗电；</p>
<p>（2）HTTPS连接缓存不如HTTP高效，会增加数据开销和功耗，甚至已有的安全措施也会因此而受到影响；</p>
<p>（3）SSL证书需要钱，功能越强大的证书费用越高，个人网站、小网站没有必要一般不会用。</p>
<p>（4）SSL证书通常需要绑定IP，不能在同一IP上绑定多个域名，IPv4资源不可能支撑这个消耗。</p>
<p>（5）HTTPS协议的加密范围也比较有限，在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。最关键的，SSL证书的信用链体系并不安全，特别是在某些国家可以控制CA根证书的情况下，中间人攻击一样可行。</p>
</li>
</ol>
<h2 id="1-31-https为什么采用混合加密机制？">1.31 https为什么采用混合加密机制？</h2>
<p><strong>参考回答</strong></p>
<p>一方面，第一阶段的非对称加密，保证了对称密钥的安全性；另一方面，第二阶段的对称加密，可以提高加密/解密处理的速度，提高数据传输的效率。</p>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p><strong>为什么需要加密？</strong></p>
<p>因为http的内容是明文传输的，明文数据会经过中间代理服务器、路由器、wifi热点、通信服务运营商等多个物理节点，如果信息在传输过程中被劫持，传输的内容就完全暴露了，他还可以篡改传输的信息且不被双方察觉，这就是<strong>中间人攻击</strong>。所以我们才需要对信息进行加密。最简单容易理解的就是<strong>对称加密</strong>。</p>
</li>
<li>
<p><strong>什么是对称加密？</strong></p>
<p>就是有一个密钥，它可以对一段内容加密，加密后只能用它才能解密看到原本的内容，和我们日常生活中用的钥匙作用差不多。</p>
</li>
<li>
<p><strong>用对称加密可行吗？</strong></p>
<p><strong>如果通信双方都各自持有同一个密钥，且没有别人知道，这两方的通信安全当然是可以被保证的（除非密钥被破解）。<strong>然而最大的问题就是</strong>这个密钥怎么让传输的双方知晓，同时不被别人知道</strong>。如果由服务器生成一个密钥并传输给浏览器，那这个传输过程中密钥被别人劫持弄到手了怎么办？之后他就能用密钥解开双方传输的任何内容了，所以这么做当然不行。<br>
​ 换种思路？试想一下，如果浏览器内部就预存了网站A的密钥，且可以确保除了浏览器和网站A，不会有任何外人知道该密钥，那理论上用对称加密是可以的，这样浏览器只要预存好世界上所有HTTPS网站的密钥就行啦！这么做显然不现实。<br>
​ 所以我们就需要神奇的<strong>非对称加密</strong>。</p>
</li>
<li>
<p><strong>什么是非对称加密？</strong></p>
<p>有两把密钥，通常一把叫做公钥、一把叫做私钥，用公钥加密的内容必须用私钥才能解开，同样，私钥加密的内容只有公钥能解开。</p>
</li>
<li>
<p><strong>用非对称加密可行吗？</strong></p>
<p><code>鉴于非对称加密的机制，我们可能会有这种思路：服务器先把公钥直接明文传输给浏览器，之后浏览器向服务器传数据前都先用这个公钥加密好再传，这条数据的安全似乎可以保障了！**因为只有服务器有相应的私钥能解开这条数据**。``  ``然而**由服务器到浏览器的这条路怎么保障安全？**如果服务器用它的的私钥加密数据传给浏览器，那么浏览器用公钥可以解密它，而这个公钥是一开始通过明文传输给浏览器的，这个公钥被谁劫持到的话，他也能用该公钥解密服务器传来的信息了。所以**目前似乎只能保证由浏览器向服务器传输数据时的安全性**（其实仍有漏洞，下文会说）。</code></p>
</li>
<li>
<p><strong>混合加密</strong></p>
<p>非对称加密耗时，非对称加密+对称加密结合可以吗？而且得尽量减少非对称加密的次数。当然是可以的，而且非对称加密、解密各只需用一次即可。以下就是加密过程：</p>
<p>（1）某网站拥有用于非对称加密的公钥A、私钥A’。</p>
<p>（2）浏览器像网站服务器请求，服务器把公钥A明文给传输浏览器。</p>
<p>（3）浏览器随机生成一个用于对称加密的密钥X，用公钥A加密后传给服务器。</p>
<p>（4）服务器拿到后用私钥A’解密得到密钥X。</p>
<p>（5）这样双方就都拥有密钥X了，且别人无法知道它。之后双方所有数据都用密钥X加密解密。</p>
<p>完美！HTTPS基本就是采用了这种方案。</p>
</li>
</ol>
<h2 id="1-32-https支持什么加密算法？">1.32 https支持什么加密算法？</h2>
<p><strong>参考回答</strong></p>
<p>常见的<strong>对称加密算法</strong>有：DES、3DES、Blowfish、IDEA、RC4、RC5、RC6和AES ；</p>
<p>常见的<strong>非对称加密算法</strong>有：RSA、ECC（移动设备用）、Diffie-Hellman、El Gamal、DSA（数字签名用）；</p>
<p>常见的<strong>Hash算法</strong>有：MD2、MD4、MD5、HAVAL、SHA；</p>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p><strong>对称加密技术</strong></p>
<p>对称加密采用了对称密码编码技术，它的<strong>特点</strong>是文件加密和解密使用相同的密钥加密。也就是密钥也可以用作解密密钥，这种方法在密码学中叫做<strong>对称加密算法</strong>，对称加密算法使用起来简单快捷，密钥较短，且破译困难，除了数据加密标准（DES），另一个对称密钥加密系统是国际数据加密算法（IDEA），它比DES的加密性好，而且对计算机功能要求也没有那么高。对称加密算法在电子商务交易过程中存在几个问题：</p>
<p>（1）要求提供一条安全的渠道使通讯双方在首次通讯时协商一个共同的密钥。直接的面对面协商可能是不现实而且难于实施的，所以双方可能需要借助于邮件和电话等其它相对不够安全的手段来进行协商；</p>
<p>（2）密钥的数目难于管理。因为对于每一个合作者都需要使用不同的密钥，很难适应开放社会中大量的信息交流；</p>
<p>（3）对称加密算法一般不能提供信息完整性的鉴别。它无法验证发送者和接受者的身份；</p>
<p>（4）对称密钥的管理和分发工作是一件具有潜在危险的和烦琐的过程。对称加密是基于共同保守秘密来实现的，采用对称加密技术的贸易双方必须保证采用的是相同的密钥，保证彼此密钥的交换是安全可靠的，同时还要设定防止密钥泄密和更改密钥的程序。</p>
</li>
</ol>
<p>假设两个用户需要使用对称加密方法加密然后交换数据，则用户最少需要2个密钥并交换使用，如果企业内用户有n个，则整个企业共需要n×(n-1) 个密钥，密钥的生成和分发将成为企业信息部门的恶梦。</p>
<p>常见的对称加密算法有DES、3DES、Blowfish、IDEA、RC4、RC5、RC6和AES</p>
<ol start="2">
<li>
<p><strong>非对称加密技术</strong></p>
<p>与对称加密算法不同，非对称加密算法需要两个密钥：<strong>公开密钥（publickey）和私有密钥（privatekey）</strong>。</p>
<p>公开密钥与私有密钥是一对，如果用公开密钥对数据进行加密，只有用对应的私有密钥才能解密；如果用私有密钥对数据进行加密，那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥，所以这种算法叫作非对称加密算法。</p>
<p>非对称加密算法实现机密信息交换的基本过程是：甲方生成一对密钥并将其中的一把作为公用密钥向其它方公开；得到该公用密钥的乙方使用该密钥对机密信息进行加密后再发送给甲方；甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。甲方只能用其专用密钥解密由其公用密钥加密后的任何信息。</p>
<p>非对称加密的<strong>典型应用是数字签名</strong>。</p>
<p>常见的非对称加密算法有：RSA、ECC（移动设备用）、Diffie-Hellman、El Gamal、DSA（数字签名用）</p>
<p>Hash算法（摘要算法）</p>
<ol>
<li>
<p><strong>Hash算法</strong></p>
<p>Hash算法特别的地方在于它是一种单向算法，用户可以通过hash算法对目标信息生成一段特定长度的唯一hash值，却不能通过这个hash值重新获得目标信息。因此Hash算法常用在不可还原的密码存储、信息完整性校验等。</p>
<p>常见的Hash算法有MD2、MD4、MD5、HAVAL、SHA</p>
</li>
</ol>
</li>
</ol>
<h2 id="1-33-说一说HTTPS的秘钥交换过程。">1.33 说一说HTTPS的秘钥交换过程。</h2>
<p><strong>参考回答</strong></p>
<p>HTTPS的密钥交换过程如下：</p>
<ol>
<li>
<p>客户端要访问一个网站，向支持https的服务器发起请求。</p>
</li>
<li>
<p>客户端向服务器发送自己支持的秘钥交换算法列表。</p>
</li>
<li>
<p>服务器选取一种秘钥交换算法加上CA证书返回给客户端。</p>
</li>
<li>
<p>客户端验证服务器是否合法，并生成一个随机数然后用协商好的加密算法加密生成随机秘钥，并用刚才从CA证书中拿到的公钥对其加密后发送给服务器。</p>
</li>
<li>
<p>服务器收到后用自己的私钥解密（中间人没有服务器的私钥，所以没有办法看到传输的数据，另外确认秘钥交换算法是在第一步，中间人是不知道秘钥交换算法（中间人是无法在第一步做手脚的，那等同于它自己就是一个真实客户端发起了一个新的请求，唯一一种情况攻击人有一个合法CA下发的证书，且客户端（一般为安卓设备）没有对CA下发的证书中的内容网站地址和当前请求地址做对比校验），就算攻击者有公钥，因为不知道协商协议，所以做不出来随机秘钥，顶多就是在传输过程中将报文拦截下来，乱改，但是给到服务器后，私钥是解不开乱改之后的密文的）。</p>
</li>
<li>
<p>服务器私钥解密之后，拿到对称秘钥，并且用它再加密一个信息，返回给浏览器。</p>
<p>**注意：**最关键的一步就是在客户端采用 RSA 或 Diffie-Hellman 等加密算法生成 Pre-master，这个随机秘钥是用来计算最终的对称秘钥的，用公钥加密之后攻击人是不知道这个这个随机秘钥的，只有服务器才能解的开。</p>
</li>
</ol>
<h2 id="1-34-说一说HTTPS的证书认证过程。">1.34 说一说HTTPS的证书认证过程。</h2>
<p><strong>参考回答</strong></p>
<p>HTTPS的证书认证过程如下：</p>
<ol>
<li>
<p>浏览器将自己支持的一套加密规则发送给网站。</p>
</li>
<li>
<p>网站从中选出一组加密算法与HASH算法，并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址，加密公钥，以及证书的颁发机构等信息。</p>
</li>
<li>
<p>浏览器获得网站证书之后浏览器要做以下工作：</p>
<p>（1） 验证证书的合法性（颁发证书的机构是否合法，证书中包含的网站地址是否与正在访问的地址一致等），如果证书受信任，则浏览器栏里面会显示一个小锁头，否则会给出证书不受信的提示。<br>
（2）如果证书受信任，或者是用户接受了不受信的证书，浏览器会生成一串随机数的密码，并用证书中提供的公钥加密。<br>
（3）使用约定好的HASH算法计算握手消息，并使用生成的随机数对消息进行加密，最后将之前生成的所有信息发送给网站。</p>
</li>
<li>
<p>网站接收浏览器发来的数据之后要做以下的操作：</p>
<p>（1） 使用自己的私钥将信息解密取出密码，使用密码解密浏览器发来的握手消息，并验证HASH是否与浏览器发来的一致。<br>
（2） 使用密码加密一段握手消息，发送给浏览器。</p>
</li>
<li>
<p>浏览器解密并计算握手消息的HASH，如果与服务端发来的HASH一致，此时握手过程结束，之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。</p>
</li>
</ol>
<h2 id="1-35-HTTP请求头中包含什么内容？">1.35 HTTP请求头中包含什么内容？</h2>
<p><strong>参考回答</strong></p>
<p>HTTP请求头中包含如下三个内容：</p>
<p><strong>User-Agent</strong>：产生请求的浏览器类型。</p>
<p><strong>Accept</strong>：客户端可识别的内容类型列表。</p>
<p><strong>Host</strong>：主机地址。</p>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p>请求报文(请求行/请求头/请求数据/空行)</p>
<p>（1） 请求行</p>
<p>求方法字段、URL字段和HTTP协议版本</p>
<p>例如：GET /index.html HTTP/1.1</p>
<p>get方法将数据拼接在url后面，传递参数受限</p>
<p>请求方法：</p>
<p>GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT</p>
<p>（2） 请求头(key value形式)</p>
<p>User-Agent：产生请求的浏览器类型。</p>
<p>Accept：客户端可识别的内容类型列表。</p>
<p>Host：主机地址</p>
<p>（3） 请求数据</p>
<p>post方法中，会把数据以key value形式发送请求</p>
<p>（4） 空行</p>
<p>发送回车符和换行符，通知服务器以下不再有请求头</p>
</li>
<li>
<p>响应报文(状态行、消息报头、响应正文)</p>
<p>状态行</p>
</li>
</ol>
<p>消息报头</p>
<p>响应正文</p>
<h2 id="1-36-HTTP是基于TCP还是UDP？">1.36 HTTP是基于TCP还是UDP？</h2>
<p><strong>参考回答</strong></p>
<p><strong>HTTP是基于TCP的。</strong></p>
<p>HTTP协议是建立在请求/响应模型上的。首先由客户建立一条与服务器的TCP链接，并发送一个请求到服务器，请求中包含请求方法、URI、协议版本以及 相关的MIME样式的消息。服务器响应一个状态行，包含消息的协议版本、一个成功和失败码以及相关的MIME式样的消息。<br>
​ HTTP/1.0为每一次HTTP的请求/响应建立一条新的TCP链接，因此一个包含HTML内容和图片的页面将需要建立多次的短期的TCP链接。一次TCP链接的建立将需要3次握手。<br>
​ 另 外，为了获得适当的传输速度，则需要TCP花费额外的回路链接时间（RTT）。每一次链接的建立需要这种经常性的开销，而其并不带有实际有用的数据，只是 保证链接的可靠性，因此HTTP/1.1提出了可持续链接的实现方法。HTTP/1.1将只建立一次TCP的链接而重复地使用它传输一系列的请求/响应消 息，因此减少了链接建立的次数和经常性的链接开销。</p>
<h2 id="1-37-HTTP1-1和HTTP2-0有什么区别？">1.37 HTTP1.1和HTTP2.0有什么区别？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>HTTP2.0使用了<strong>多路复用</strong>的技术，做到同一个连接并发处理多个请求，而且并发请求的数量比HTTP1.1大了好几个数量级。HTTP1.1也可以多建立几个TCP连接，来支持处理更多并发的请求，但是创建TCP连接本身也是有开销的。</p>
</li>
<li>
<p>在HTTP1.1中，HTTP请求和响应都是由状态行、请求/响应头部、消息主体三部分组成。一般而言，消息主体都会经过gzip压缩，或者本身传输的就是压缩过后的二进制文件，但状态行和头部却没有经过任何压缩，直接以纯文本传输。随着Web功能越来越复杂，每个页面产生的请求数也越来越多，导致消耗在头部的流量越来越多，尤其是每次都要传输UserAgent、Cookie这类不会频繁变动的内容，完全是一种浪费。 <strong>HTTP1.1不支持header数据的压缩，HTTP2.0使用HPACK算法对header的数据进行压缩，这样数据体积小了，在网络上传输就会更快。</strong></p>
</li>
<li>
<p>服务端推送是一种在客户端请求之前发送数据的机制。网页使用了许多资源：HTML、样式表、脚本、图片等等。在HTTP1.1中这些资源每一个都必须明确地请求。这是一个很慢的过程。浏览器从获取HTML开始，然后在它解析和评估页面的时候，增量地获取更多的资源。因为服务器必须等待浏览器做每一个请求，网络经常是空闲的和未充分使用的。</p>
<p><strong>为了改善延迟，HTTP2.0引入了server push</strong>，它允许服务端推送资源给浏览器，在浏览器明确地请求之前，免得客户端再次创建连接发送请求到服务器端获取。这样客户端可以直接从本地加载这些资源，不用再通过网络。</p>
</li>
</ol>
<h2 id="1-38-HTTP2-0和HTTP3-0有什么区别？">1.38 HTTP2.0和HTTP3.0有什么区别？</h2>
<p><strong>参考回答</strong></p>
<p>HTTP2.0和HTTP3.0的区别在于前者使用tcp协议而后者使用udp协议。</p>
<p><strong>答案解析</strong></p>
<p><strong>http发展历程：从http0.9 到 http3.0</strong></p>
<ol>
<li>
<p>HTTP0.9</p>
<p>最简单的只有请求行 GET index.html</p>
</li>
<li>
<p>HTTP1.0</p>
<p>（1）增加请求头、响应头，让请求和相应都更清晰</p>
<p>（2）增加状态码，让响应更清晰</p>
<p>（3）增加缓存功能，已请求过的内容再次请求时就可直接使用缓存</p>
</li>
</ol>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs xml">GET index.html HTTP/1.0 <br>accept: application/html<br>accept-charset: utf-8 <br>accept-encoding: gzip <br>accept-language: zh-CN<br>HTTP/1.0 200 OK <br><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span> <br><span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span> <br><span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span> <br><span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>hello world!<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span> <br><span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span><br></code></pre></td></tr></table></figure>
<p>￼a. accept 解决文件格式问题，是json还是html，浏览器根据不同文件格式来解析文件；</p>
<p>b. accept-charset 解决文件编码问题，告知浏览器如何将字符流解析成字节流；</p>
<p>c. accept-encoding 解决大文件压缩问题，浏览器采用指定的解压方式来解压；</p>
<p>d. accept-language 解决国际化问题，不同国家请求不同语言的文件。</p>
<ol>
<li>
<p>HTTP1.1</p>
<p>（1）持久连接，多个http请求使用同一个tcp连接，减少了tcp建立连接时的开销</p>
<p>（2）客户端和服务器之间可以建立多个tcp连接以解决队头阻塞的问题</p>
<p>（3）响应体可以分块传输，无需一次传输全部内容</p>
<p>（4）响应头增加content-length字段满足动态内容无法一次计算出长度和无法一次传输完成的问题</p>
<p>（5）增加了安全机制和cookie机制</p>
</li>
<li>
<p>HTTP2.0<br>
多路复用，客户端和服务器之间只建立一条tcp，每个http请求被切分成多帧，多个http的帧混合在一起在一个tcp连接上传送</p>
</li>
<li>
<p>HTTP3.0<br>
不再使用tcp协议，因为tcp依然是顺序发送，顺序接收的，依然有队头堵塞问题，干掉tcp才能解决队头堵塞问题。google的QUIC就使用了udp协议。</p>
</li>
</ol>
<h2 id="1-39-谈谈HTTP的缓存机制，服务器如何判断当前缓存过期？">1.39 谈谈HTTP的缓存机制，服务器如何判断当前缓存过期？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>HTTP报文</p>
<p>在浏览器和服务器进行Http通信时发送的数据即为Http报文，其中分为两部分：</p>
<p>（1）header - 报文的首部或头部，其中保存着各类请求的属性字段，关于Http的缓存相关规则信息均保存在header中；</p>
<p>（2）body - 请求体部分，Http请求真正传输的主体部分。</p>
</li>
<li>
<p>首次请求基本规则</p>
<p>HTTP缓存<strong>主要涉及三个角色</strong>：一是浏览器，二是浏览器的缓存数据库，三是服务器。当浏览器端向服务器发出<strong>第一次</strong>请求时的流程图如下图所示（浏览器再次执行同一的请求时，根据不同的缓存类型将会执行不同的行为）：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%90%91%E5%AE%A2%E6%88%B7%E7%AB%AF%E9%A6%96%E6%AC%A1%E8%AF%B7%E6%B1%82.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="浏览器向客户端首次请求">
<p>浏览器向服务器发出第一次请求后执行流程</p>
</li>
<li>
<p>缓存的类型</p>
<p>HTTP缓存主要分为两种：<strong>强缓存</strong>和<strong>协商缓存</strong>。<br>
​ 两种缓存分别通过HTTP报文头部不同的字段进行控制。</p>
</li>
<li>
<p>服务器如何判断当前缓存过期</p>
<p>（1）<strong>强缓存</strong></p>
<p>强缓存<strong>基本原理</strong>是：所请求的数据在缓存数据库中尚未过期时，不与服务器进行交互，直接使用缓存数据库中的数据。当缓存未命中时，则重新向服务器请求数据，其基本流程分别如下：</p>
<p>当缓存<strong>未过期</strong>：</p>
<p>当缓存<strong>未命中</strong>（基本流程与首次请求时相似）：</p>
<p><strong>控制强缓存过期时间的主要有两个规则字段</strong>，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6%E5%BC%BA%E7%BC%93%E5%AD%98.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="缓存机制强缓存">
<p><strong>Expire：</strong> 其指定了一个日期/时间， 在这个日期/时间之后，HTTP响应被认为是过时的。但是它本身是一个HTTP1.0标准下的字段，所以<strong>如果请求中还有一个置了 “max-age” 或者 “s-max-age” 指令的Cache-Control响应头，那么 Expires 头就会被忽略。</strong><br>
​ **Cache-Control：**通用消息头用于在http 请求和响应中通过指定指令来实现缓存机制。其常用的几个取值有：<br>
​ private：客户端可以缓存<br>
​ public：客户端和代理服务器都可以缓存<br>
​ max-age=xxx：缓存的内容将在xxx 秒后失效<br>
​ s-max-age=xxx：同s-max-age，但仅适用于共享缓存(比如各个代理)，并且私有缓存中忽略。<br>
​ no-cache：需要使用协商缓存来验证缓存数据<br>
​ no-store：所有内容都不会缓存，强缓存和协商缓存都不会触发<br>
​ must-revalidate：缓存必须在使用之前验证旧资源的状态，并且不可使用过期资源。</p>
<p>（2）<strong>协商缓存</strong></p>
<p>当强缓存过期未命中或者响应报文Cache-Control中有must-revalidate标识必须每次请求验证资源的状态时，便使用协商缓存的方式去处理缓存文件。<br>
​ 协商缓存<strong>主要原理</strong>是：从缓存数据库中取出缓存的标识，然后向浏览器发送请求验证请求的数据是否已经更新，如果已更新则返回新的数据，若未更新则使用缓存数据库中的缓存数据，具体流程如下：</p>
<p>当协商缓存<strong>命中</strong>：</p>
<p>协商缓存<strong>未命中</strong>：</p>
<p>结合具体的请求来看，首先是第一次发送某请求后服务器的response：</p>
<p>两个字段etag和last-modified是用于协商缓存的规则字段。其中etag是所请求的数据在服务器中的唯一标识，而last-modifind标识所请求资源最后一次修改的时间。<br>
在缓存时间3600秒过去之后，我们再次发起同样的请求：</p>
<p>可以看到，在我们的请求中有这样两个字段if-modifind-since和if-none-match，两个字段分别对应着响应中的last-Modified和etag，用来对协商缓存进行判断：</p>
<p>a. 首先，如果在第一次请求中有etag和last-modified时，缓存数据库会保存这两个字段，并且在再次发起同样的请求时以if-none-match和if-modified-since发送保存的last-modified和etag数据。</p>
<p>b. 服务器收到请求后会以<strong>优先级</strong>if-none-match &gt; if-modifind-since的顺序进行判断，如果资源的etag和if-none-match相等，即所请求的资源没有变化，此时浏览器即可以使用缓存数据库中的数据，此时http的请求状态码为304，请求的资源未变化。</p>
<p>c. 如果请求字段中没有if-none-match，就使用if-modified-since来判断。如果if-modified-since的值和所请求的资源时间一致，即所请求的资源相同，浏览器即可以使用缓存数据库中的数据。http状态码304。</p>
</li>
<li>
<p>浏览器缓存机制流程图</p>
</li>
</ol>
<h2 id="1-40-介绍一下HTTP协议中的长连接和短连接。">1.40 介绍一下HTTP协议中的长连接和短连接。</h2>
<p><strong>参考回答</strong></p>
<p>HTTP协议的底层使用TCP协议，所以HTTP协议的长连接和短连接在本质上是TCP层的长连接和短连接。由于TCP建立连接、维护连接、释放连接都是要消耗一定的资源，浪费一定的时间。所对于服务器来说，频繁的请求释放连接会浪费大量的时间，长时间维护太多的连接的话又需要消耗资源。所以长连接和短连接并不存在优劣之分，只是适用的场合不同而已。长连接和短连接分别有如下优点和缺点：</p>
<p>**长连接优点：**可以节省较多的TCP连接和释放的操作，节约时间，对于频繁请求资源的用户来说，适合长连接。</p>
<p>**长连接缺点：**由于有保活功能，当遇到大量的恶意连接时，服务器的压力会越来越大。这时服务器需要采取一些策略，关闭一些长时间没有进行读写事件的的连接。</p>
<p>**短连接优点：**短连接对服务器来说管理比较简单，只要存在的连接都是有效连接，不需要额外的控制手段，而且不会长时间占用资源 。</p>
<p>**短连接缺点：**如果客户端请求频繁的话，会在TCP的建立和释放上浪费大量的时间。</p>
<p><strong>注意：<strong>从</strong>HTTP/1.1版本起</strong>，默认使用长连接用以保持连接特性。使用长连接的HTTP协议，会在响应消息报文段加入: Connection: keep-alive。TCP中也有keep alive，但是TCP中的keep alive只是探测TCP连接是否活着，而HTTP中的keep-alive是让一个TCP连接获得更久一点。</p>
<h2 id="1-41-介绍一下HTTPS的流程。">1.41 介绍一下HTTPS的流程。</h2>
<p><strong>参考回答</strong></p>
<p>**HTTPS在传输的过程中会涉及到三个密钥：**服务器端的公钥和私钥，用来进行非对称加密；客户端生成的随机密钥，用来进行对称加密。一个HTTPS请求实际上包含了两次HTTP传输，如下图可以细分为以下8步：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/view.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="preview">
<p><img src="https:////upload-images.jianshu.io/upload_images/16749538-3ae48d5925636dc1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp" srcset="/crystal575/img/loading.gif" lazyload alt="img"></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/HTTP8%E6%AD%A5%E6%B5%81%E7%A8%8B%E5%9B%BE.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="HTTP8步流程图">
<ol>
<li>客户端向服务器发起HTTPS请求，连接到服务器的443端口</li>
<li>服务器端有一个密钥对，即公钥和私钥，是用来进行非对称加密使用的，服务器端保存着私钥，不能将其泄露，公钥可以发送给任何人。</li>
<li>服务器将自己的公钥发送给客户端。</li>
<li>客户端收到服务器端的公钥之后，会对公钥进行检查，验证其合法性，如果发现发现公钥有问题，那么HTTPS传输就无法继续。严格的说，这里应该是验证服务器发送的数字证书的合法性，关于客户端如何验证数字证书的合法性，下文会进行说明。如果公钥合格，那么客户端会生成一个随机值，这个随机值就是用于进行对称加密的密钥，我们将该密钥称之为client key，即客户端密钥，这样在概念上和服务器端的密钥容易进行区分。然后用服务器的公钥对客户端密钥进行非对称加密，这样客户端密钥就变成密文了，至此，HTTPS中的第一次HTTP请求结束。</li>
<li>客户端会发起HTTPS中的第二个HTTP请求，将加密之后的客户端密钥发送给服务器。</li>
<li>服务器接收到客户端发来的密文之后，会用自己的私钥对其进行非对称解密，解密之后的明文就是客户端密钥，然后用客户端密钥对数据进行对称加密，这样数据就变成了密文。</li>
<li>然后服务器将加密后的密文发送给客户端。</li>
<li>客户端收到服务器发送来的密文，用客户端密钥对其进行对称解密，得到服务器发送的数据。这样HTTPS中的第二个HTTP请求结束，整个HTTPS传输完成。</li>
</ol>
<h2 id="1-42-介绍一下HTTP的失败码。">1.42 介绍一下HTTP的失败码。</h2>
<p><strong>参考回答</strong></p>
<p>HTTP的错误码包含<strong>客户端错误4XX</strong> 和<strong>服务端错误5XX</strong> ，两种错误分别如下：</p>
<ol>
<li>
<p>客户端错误 4XX</p>
<p>这类的状态码是适用于客户端似乎有错误的情况。除了响应给HEAD请求外，服务器应该包含一个包括错误情况描述的实体，和它是暂时的还是永久性的。这些状态码适用于任何请求方法。用户代理应该展示所有包含的实体给用户。</p>
<p>如果客户端正在发送数据，使用TCP的服务器应该在服务器关闭输出链接时，仔细确保客户端确认收到包含响应的数据包（receipt of the packet(s) ） 。如果客户端继续在服务器关闭后发送数据，服务器的TCP栈将会发生一个重置包给客户端，这可能会在 HTTP 应用程序读取和解释客户端的未确认输入缓冲区（input buffers）之前将其擦除。</p>
<p><strong>400</strong>(错误请求) 服务器不理解请求的语法。</p>
<p><strong>401</strong>(未授权) 请求要求进行身份验证。登录后，服务器可能会返回对页面的此响应。</p>
<p><strong>403</strong>(已禁止) 服务器拒绝请求。如果在 Googlebot 尝试抓取您网站上的有效网页时显示此状态代码(您可在 Google 网站管理员工具中诊断下的网络抓取页面上看到此状态代码)，那么，这可能是您的服务器或主机拒绝 Googlebot 对其进行访问。</p>
<p><strong>404</strong>(未找到) 服务器找不到请求的网页。例如，如果请求是针对服务器上不存在的网页进行的，那么，服务器通常会返回此代码。</p>
<p>如果您的网站上没有 robots.txt 文件，而您在 Google 网站管理员工具”诊断”标签的 robots.txt 页上发现此状态，那么，这是正确的状态。然而，如果您有 robots.txt 文件而又发现了此状态，那么，这说明您的 robots.txt 文件可能是命名错误或位于错误的位置。(该文件应当位于顶级域名上，且应当名为 robots.txt)。</p>
<p>如果您在 Googlebot 尝试抓取的网址上发现此状态(位于”诊断”标签的 HTTP 错误页上)，那么，这表示 Googlebot 所追踪的可能是另一网页中的无效链接(旧链接或输入有误的链接)。</p>
<p><strong>405</strong>(方法禁用) 禁用请求中所指定的方法。</p>
<p><strong>406</strong>(不接受) 无法使用请求的内容特性来响应请求的网页。</p>
<p><strong>407</strong>(需要代理授权) 此状态代码与 401(未授权)类似，但却指定了请求者应当使用代理进行授权。如果服务器返回此响应，那么，服务器还会指明请求者应当使用的代理。</p>
<p><strong>408</strong>(请求超时) 服务器等候请求时超时。</p>
<p><strong>409</strong>(冲突) 服务器在完成请求时发生冲突。服务器必须包含有关响应中所发生的冲突的信息。服务器在响应与前一个请求相冲突的 PUT 请求时可能会返回此代码，同时会提供两个请求的差异列表。</p>
<p><strong>410</strong>(已删除) 如果请求的资源已被永久删除，那么，服务器会返回此响应。该代码与 404(未找到)代码类似，但在资源以前有但现在已经不复存在的情况下，有时会替代 404 代码出现。如果资源已被永久删除，那么，您应当使用 301 代码指定该资源的新位置。</p>
<p><strong>411</strong>(需要有效长度) 服务器不会接受包含无效内容长度标头字段的请求。</p>
<p><strong>412</strong>(未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。</p>
<p><strong>413</strong>(请求实体过大) 服务器无法处理请求，因为请求实体过大，已超出服务器的处理能力。</p>
<p><strong>414</strong>(请求的 URI 过长) 请求的 URI(通常为网址)过长，服务器无法进行处理。</p>
<p><strong>415</strong>(不支持的媒体类型) 请求的格式不受请求页面的支持。</p>
<p><strong>416</strong>(请求范围不符合要求) 如果请求是针对网页的无效范围进行的，那么，服务器会返回此状态代码。</p>
<p><strong>417</strong>(未满足期望值) 服务器未满足”期望”请求标头字段的要求。</p>
</li>
<li>
<p>服务端错误 5XX</p>
<p>响应状态码已数字5开头，表明了这类服务器知道其错误或者无法执行请求的情况。出了响应HEAD请求外，服务器应该包括一个包含错误情况说明的实体，以及他是暂时地还是永久性的，用户代理应该将所有包含的实体展示给用户。这些响应代码适用于任何请求方法。</p>
<p><strong>500</strong>(服务器内部错误) 服务器遇到错误，无法完成请求。</p>
<p><strong>501</strong>(尚未实施) 服务器不具备完成请求的功能。例如，当服务器无法识别请求方法时，服务器可能会返回此代码。</p>
<p><strong>502</strong>(错误网关) 服务器作为网关或代理，从上游服务器收到了无效的响应。</p>
<p><strong>503</strong>(服务不可用) 目前无法使用服务器(由于超载或进行停机维护)。通常，这只是一种暂时的状态。</p>
<p><strong>504</strong>(网关超时) 服务器作为网关或代理，未及时从上游服务器接收请求。</p>
<p><strong>505</strong>(HTTP 版本不受支持) 服务器不支持请求中所使用的 HTTP 协议版本。</p>
</li>
</ol>
<h2 id="1-43-说一说你知道的http状态码。">1.43 说一说你知道的http状态码。</h2>
<p><strong>参考回答</strong></p>
<p>HTTP状态码由三个十进制数字组成，第一个十进制数字定义了状态码的类型，后两个数字没有分类的作用。HTTP状态码共分为5种类型，分类及分类描述如下表：</p>
<table>
<thead>
<tr>
<th style="text-align:center">分类</th>
<th style="text-align:center">分类描述</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">1**</td>
<td style="text-align:center">信息，服务器收到请求，需要请求者继续执行操作</td>
</tr>
<tr>
<td style="text-align:center">2**</td>
<td style="text-align:center">成功，操作被成功接收并处理</td>
</tr>
<tr>
<td style="text-align:center">3**</td>
<td style="text-align:center">重定向，需要进一步的操作以完成请求</td>
</tr>
<tr>
<td style="text-align:center">4**</td>
<td style="text-align:center">客户端错误，请求包含语法错误或无法完成请求</td>
</tr>
<tr>
<td style="text-align:center">5**</td>
<td style="text-align:center">服务器错误，服务器在处理请求的过程中发生了错误</td>
</tr>
</tbody>
</table>
<p><strong>各类别常见状态码</strong>有如下几种：</p>
<ol>
<li>
<p><strong>2xx （3种）</strong></p>
<p>**200 OK：**表示从客户端发送给服务器的请求被正常处理并返回；</p>
<p>**204 No Content：**表示客户端发送给客户端的请求得到了成功处理，但在返回的响应报文中不含实体的主体部分（没有资源可以返回）；</p>
<p>**206 Patial Content：**表示客户端进行了范围请求，并且服务器成功执行了这部分的GET请求，响应报文中包含由Content-Range指定范围的实体内容。</p>
</li>
<li>
<p><strong>3xx （5种）</strong></p>
<p>**301 Moved Permanently：**永久性重定向，表示请求的资源被分配了新的URL，之后应使用更改的URL；</p>
<p>**302 Found：**临时性重定向，表示请求的资源被分配了新的URL，希望本次访问使用新的URL；</p>
<p>301与302的区别：前者是永久移动，后者是临时移动（之后可能还会更改URL）</p>
<p>**303 See Other：**表示请求的资源被分配了新的URL，应使用GET方法定向获取请求的资源；</p>
<p>302与303的区别：后者明确表示客户端应当采用GET方式获取资源</p>
<p>**304 Not Modified：**表示客户端发送附带条件（是指采用GET方法的请求报文中包含if-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since中任一首部）的请求时，服务器端允许访问资源，但是请求为满足条件的情况下返回该状态码；</p>
<p>**307 Temporary Redirect：**临时重定向，与303有着相同的含义，307会遵照浏览器标准不会从POST变成GET；（不同浏览器可能会出现不同的情况）；</p>
</li>
<li>
<p><strong>4xx （4种）</strong></p>
<p>**400 Bad Request：**表示请求报文中存在语法错误；</p>
<p>**401 Unauthorized：**未经许可，需要通过HTTP认证；</p>
<p>**403 Forbidden：**服务器拒绝该次访问（访问权限出现问题）</p>
<p>**404 Not Found：**表示服务器上无法找到请求的资源，除此之外，也可以在服务器拒绝请求但不想给拒绝原因时使用；</p>
</li>
<li>
<p><strong>5xx （2种）</strong></p>
<p>**500 Inter Server Error：**表示服务器在执行请求时发生了错误，也有可能是web应用存在的bug或某些临时的错误时；</p>
<p>**503 Server Unavailable：**表示服务器暂时处于超负载或正在进行停机维护，无法处理请求；</p>
</li>
</ol>
<h2 id="1-44-301和302有什么区别？">1.44 301和302有什么区别？</h2>
<p><strong>参考回答</strong></p>
<p>301和302的区别在于，<strong>301重定向是永久的重定向</strong>，搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址。<strong>302重定向是暂时的重定向</strong>，搜索引擎会抓取新的内容而保存旧的网址。由于效劳器前往302代码，搜索引擎以为新的网址只是暂时的。</p>
<h2 id="1-45-302和304有什么区别？">1.45 302和304有什么区别？</h2>
<p><strong>参考回答</strong></p>
<p>302和304是网页请求的两个不同的响应状态码。302 （临时移动）表示 服务器目前从不同位置的网页响应请求，但请求者应继续使用原有位置来进行以后的请求。 304 （未修改）表示 自从上次请求后，请求的网页未修改过。 服务器返回此响应时，不会返回网页内容。</p>
<h2 id="1-46-请描述一次完整的HTTP请求的过程。">1.46 请描述一次完整的HTTP请求的过程。</h2>
<p><strong>参考回答</strong></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/DNS%E8%A7%A3%E6%9E%90%E6%B5%81%E7%A8%8B%E5%9B%BE.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="DNS解析流程图">
<p>DNS解析流程图</p>
<ol>
<li>首先客户端位置是一台电脑或手机，在打开浏览器以后，比如输入<a target="_blank" rel="noopener" href="http://www.zdns.xn--cn%2Cdns%2C%2C%2C%2Chttp-766vogusha7cy15ax4al23aja921cnqav8ghabbc615getb5hnfi356siajcde171flja68x3a963qgluw0bo63gvcay81rwvcuy8e2a603jladjg83ph8klvaka497b5xgfar4131emba165lw65kg6tambgd4149mg0cnwgtwitx6uy1b9y0eondv2bwb790tnd8dm4okj1j6juaelx9a//www.zdns.cn%E7%9A%84%E6%97%B6%E5%80%99%EF%BC%8C%E6%A0%B9%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%B0%B1%E4%BC%9A%E8%BF%94%E5%9B%9E.cn%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E4%BD%8D%E7%BD%AE%E4%BF%A1%E6%81%AF%EF%BC%9B">http://www.zdns.cn的域名，它首先是由浏览器发起一个DNS解析请求，如果本地缓存服务器中找不到结果，则首先会向根服务器查询，根服务器里面记录的都是各个顶级域所在的服务器的位置，当向根服务器请求http://www.zdns.cn的时候，根服务器就会返回.cn服务器的位置信息；</a></li>
<li>递归服务器拿到.cn的权威服务器地址以后，就会寻问.cn的权威服务器，知不知道<a target="_blank" rel="noopener" href="http://www.zdns.xn--cn-go3c700mblj.xn--ciqy0cz91bpj2b.xn--cnhttp-cf4jr3wm4am3ux8m2xlnnotva89ep67y//zdns.cn%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E5%9C%B0%E5%9D%80%EF%BC%9B">http://www.zdns.cn的位置。这个时候.cn权威服务器查找并返回http://zdns.cn服务器的地址；</a></li>
<li>继续向<a target="_blank" rel="noopener" href="http://zdns.xn--cn%2Chttp-s33kz54auwcjzkwoc7fk39ap54cwya32f0w0hyif6z8j9nra//zdns.cn%E7%9A%84%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BB%99%E5%87%BA%E4%BA%86%E5%9C%B0%E5%9D%80%EF%BC%9A202.173.11.10%EF%BC%9B">http://zdns.cn的权威服务器去查询这个地址，由http://zdns.cn的服务器给出了地址：202.173.11.10；</a></li>
<li>最终进入http的链接，顺利访问网站；</li>
</ol>
<p><strong>补充说明</strong>：一旦递归服务器拿到解析记录以后，就会在本地进行缓存，如果下次客户端再请求本地的递归域名服务器相同域名的时候，就不会再这样一层一层查了，因为本地服务器里面已经有缓存了，这个时候就直接把<a target="_blank" rel="noopener" href="http://www.zdns.xn--cn-e62cpo61szye9nky6ax4q6vhin4d5cldmmzy7bl8m./">http://www.zdns.cn的记录返回给客户端就可以了。</a></p>
<h2 id="1-47-什么是重定向？">1.47 什么是重定向？</h2>
<p><strong>参考回答</strong></p>
<p>**重定向(Redirect)**就是通过各种方法将各种网络请求重新定个方向转到其它位置（如：网页重定向、域名的重定向、路由选择的变化也是对数据报文经由路径的一种重定向）。</p>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p>需要重定向的情况</p>
<p>（1）网站调整（如改变网页目录结构）；</p>
<p>（2）网页被移到一个新地址；</p>
<p>（3）网页扩展名改变(如应用需要把.php改成.Html或.shtml)。</p>
<p>这几种情况下，如果不做重定向，则用户收藏夹或搜索引擎数据库中旧地址只能让访问客户得到一个404 页面错误信息，访问流量白白丧失；再者某些注册了多个域名的网站，也需要通过重定向让访问这些域名的用户自动跳转到主站点等。</p>
</li>
<li>
<p>常用的重定向的方式</p>
<p>（1）301 redirect-----永久性转移</p>
<p>当用户或搜索引擎向网站服务器发出浏览请求时，服务器返回的HTTP数据流中头信息(header)中的状态码的一种，表示本网页永久性转移到另一个地址。</p>
<p>（2）302 redirect-----暂时性转移 (Temporarily Moved )</p>
<p>也被认为是<strong>暂时重定向</strong>（temporary redirect），一条对网站浏览器的指令来显示浏览器被要求显示的不同的URL，当一个网页经历过短期的URL的变化时使用。一个暂时重定向是一种服务器端的重定向，能够被搜索引擎蜘蛛正确地处理。</p>
</li>
<li>
<p>新旧重定向方式的区别</p>
<p><strong>302重定向是暂时的重定向</strong>，搜索引擎会抓取新的内容而保存旧的网址。由于效劳器前往302代码，搜索引擎以为新的网址只是暂时的；</p>
<p><strong>301重定向是永久的重定向</strong>，搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址。</p>
</li>
<li>
<p>为什么302 重定向和网址劫持有关联</p>
<p>从网址A 做一个302 重定向到网址B 时，主机服务器的隐含意思是网址A 随时有可能改主意，重新显示本身的内容或转向其他的地方。大部分的搜索引擎在大部分情况下，当收到302 重定向时，一般只要去抓取目标网址就可以了，也就是说网址B。如果搜索引擎在遇到302 转向时，百分之百的都抓取目标网址B 的话，就不用担心网址URL 劫持了。问题就在于，有的时候搜索引擎，尤其是Google，并不能总是抓取目标网址。</p>
<p>比如说，有的时候A 网址很短，但是它做了一个302 重定向到B 网址，而B 网址是一个很长的乱七八糟的URL 网址，甚至还有可能包含一些问号之类的参数。很自然的，A 网址更加用户友好，而B 网址既难看，又不用户友好。这时Google 很有可能会仍然显示网址A。由于搜索引擎排名算法只是程序而不是人，在遇到302 重定向的时候，并不能像人一样的去准确判定哪一个网址更适当，这就造成了网址URL 劫持的可能性。也就是说，一个不道德的人在他自己的网址A 做一个302 重定向到你的网址B，出于某种原因， Google 搜索结果所显示的仍然是网址A，但是所用的网页内容却是你的网址B 上的内容，这种情况就叫做网址URL 劫持。你辛辛苦苦所写的内容就这样被别人偷走了。</p>
<p>302 重定向所造成的网址URL 劫持现象，已经存在一段时间了。不过到目前为止，似乎也没有什么更好的解决方法。在正在进行的数据中心转换中，302 重定向问题也是要被解决的目标之一。从一些搜索结果来看，网址劫持现象有所改善，但是并没有完全解决。</p>
</li>
</ol>
<h2 id="1-48-重定向和请求转发有什么区别？">1.48 重定向和请求转发有什么区别？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>请求转发</p>
<p>客户首先发送一个请求到服务器端，服务器端发现匹配的servlet，并指定它去执行，当这个servlet执行完之后，它要调用getRequestDispacther()方法，把请求转发给指定的student_list.jsp,整个流程都是在服务器端完成的，而且是在同一个请求里面完成的，因此servlet和jsp共享的是同一个request，在servlet里面放的所有东西，在student_list中都能取出来，因此，student_list能把结果getAttribute()出来，getAttribute()出来后执行完把结果返回给客户端。整个过程是一个请求，一个响应。</p>
</li>
<li>
<p>重定向</p>
<p>客户发送一个请求到服务器，服务器匹配servlet，servlet处理完之后调用了sendRedirect()方法，立即向客户端返回这个响应，响应行告诉客户端你必须要再发送一个请求，去访问student_list.jsp，紧接着客户端收到这个请求后，立刻发出一个新的请求，去请求student_list.jsp,这里两个请求互不干扰，相互独立，在前面request里面setAttribute()的任何东西，在后面的request里面都获得不了。可见，在sendRedirect()里面是两个请求，两个响应。（服务器向浏览器发送一个302状态码以及一个location消息头，浏览器收到请求后会向再次根据重定向地址发出请求）</p>
</li>
<li>
<p><strong>二者区别</strong></p>
<p>（1）请求次数：重定向是浏览器向服务器发送一个请求并收到响应后再次向一个新地址发出请求，转发是服务器收到请求后为了完成响应跳转到一个新的地址；重定向至少请求两次，转发请求一次；</p>
<p>（2）地址栏不同：重定向地址栏会发生变化，转发地址栏不会发生变化；</p>
<p>（3）是否共享数据：重定向两次请求不共享数据，转发一次请求共享数据（在request级别使用信息共享，使用重定向必然出错）；</p>
<p>（4）跳转限制：重定向可以跳转到任意URL，转发只能跳转本站点资源；</p>
<p>（5）发生行为不同：重定向是客户端行为，转发是服务器端行为。</p>
</li>
</ol>
<h2 id="1-50-说一说你对TIME-WAIT的理解。">1.50 说一说你对TIME_WAIT的理解。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>出现 TIME_WAIT的状态原因</p>
<p>TIME_WAIT状态之所以存在,是为了保证网络的可靠性。由于TCP连接是双向的，所以在关闭连接的时候，两个方向各自都需要关闭。先发FIN包的一方执行的是主动关闭，后发送FIN包的一方执行的是被动关闭。主动关闭的一方会进入TIME_WAIT状态，并且在此状态停留2MSL时长。如果Server端一直没有向client端发送FIN消息(调用close() API)，那么这个CLOSE_WAIT会一直存在下去。</p>
</li>
<li>
<p>MSL概念</p>
<p>其指的是报文段的最大生存时间。如果报文段在网络中活动了MSL时间，还没有被接收，那么就会被丢弃。关于MSL的大小，RFC 793协议中给出的建议是2分钟，不过Linux中，通常是半分钟。</p>
</li>
<li>
<p><strong>TIME_WAIT持续两个MSL的作用</strong></p>
<p>首先，可靠安全地关闭TCP连接。比如网络拥塞，如果主动关闭方最后一个ACK没有被被动关闭方接收到，这时被动关闭方会对FIN进行超时重传，在这时尚未关闭的TIME_WAIT就会把这些尾巴问题处理掉，不至于对新连接及其他服务产生影响。其次，防止由于没有持续TIME_WAIT时间导致的新的TCP连接建立起来，延迟的FIN重传包会干扰新的连接。</p>
</li>
<li>
<p>TIME_WAIT占用的资源</p>
<p>少量内存（大概4K）和一个文件描述符fd。</p>
</li>
<li>
<p><strong>TIME_WAIT关闭的危害</strong></p>
<p>首先，当网络情况不好时，如果主动方无TIME_WAIT等待，关闭前个连接后，主动方与被动方又建立起新的TCP连接，这时被动方重传或延时过来的FIN包到达后会直接影响新的TCP连接；其次，当网络情况不好时，同时没有TIME_WAIT等待时，关闭连接后无新连接，那么当接收到被动方重传或延迟的FIN包后，会给被动方回送一个RST包，可能会影响被动方其他的服务连接。</p>
</li>
</ol>
<p><strong>答案解析</strong></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/timewait.PNG" srcset="/crystal575/img/loading.gif" lazyload class="" title="timewait">
<p>当client端传输完成数据，或者需要断开连接时：</p>
<ol>
<li>Client端发送一个FIN报文给Server端。表示要终止Client到Server这个方向的连接。通过调用close(socket) API。表示Client不再会发送数据到Server端。(但Server还能继续发给Client端)。Client状态变为FIN_WAIT_1。</li>
<li>Server端收到FIN后，发送一个ACK报文给Client端(序号为M+1)。Server状态变为CLOSE_WAIT，Client收到序号为(M+1)的ACK后状态变为FIN_WAIT_2。Server端也发送一个FIN报文给Client端。(序号为N)<br>
表示Server也要终止到Client端这个方向的连接。通过调用close(socket) API。Server端状态变为LAST_ACK。</li>
<li>Client端收到报文FIN后，也发送一个ACK报文给服务器。(序号N+1)，Client状态变为TIME_WAIT。</li>
<li>Server端收到序号为(N+1)的ACK， Server的状态变为CLOSED。</li>
<li>等带2MSL之后，Client的状态也变为CLOSE。</li>
</ol>
<p>至此，一个完整的TCP连接就关闭了。</p>
<h2 id="1-51-TIME-WAIT、CLOSE-WAIT状态发生在哪一步？">1.51 TIME_WAIT、CLOSE_WAIT状态发生在哪一步？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p><strong>TIME_WAIT状态</strong>发生在客户端主动关闭连接时，发送最后一个ack后；<strong>CLOSE_WAIT状态</strong>发生在在Sever端收到Client的FIN消息之后。</p>
</li>
<li>
<p>出现 TIME_WAIT的状态原因</p>
<p>TIME_WAIT状态之所以存在,是为了保证网络的可靠性。由于TCP连接是双向的，所以在关闭连接的时候，两个方向各自都需要关闭。先发FIN包的一方执行的是主动关闭，后发送FIN包的一方执行的是被动关闭。主动关闭的一方会进入TIME_WAIT状态，并且在此状态停留2MSL时长。如果Server端一直没有向client端发送FIN消息(调用close() API)，那么这个CLOSE_WAIT会一直存在下去。</p>
</li>
<li>
<p>出现CLOSE_WAIT的状态原因</p>
<p>假设最终的ACK丢失，server将重发FIN，client必须维护TCP状态信息以便可以重发最终的ACK，否则会发送RST，结果server认为发生错误。TCP实现必须可靠地终止连接的两个方向(全双工关闭)，client必须进入 TIME_WAIT 状态，因为client可能面临重发最终ACK的情形。</p>
</li>
<li>
<p>为什么 TIME_WAIT 状态需要保持 2MSL 这么长的时间？</p>
<p>如果 TIME_WAIT 状态保持时间不足够长(比如小于2MSL)，第一个连接就正常终止了。第二个拥有相同相关五元组的连接出现，而第一个连接的重复报文到达，干扰了第二个连接。TCP实现必须防止某个连接的重复报文在连接终止后出现，所以让TIME_WAIT状态保持时间足够长(2MSL)，连接相应方向上的TCP报文要么完全响应完毕，要么被丢弃。建立第二个连接的时候，不会混淆。</p>
</li>
</ol>
<p><strong>答案解析</strong></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/timewait-163592736593562.PNG" srcset="/crystal575/img/loading.gif" lazyload class="" title="timewait">
<p>当client端传输完成数据，或者需要断开连接时：</p>
<ol>
<li>Client端发送一个FIN报文给Server端。表示要终止Client到Server这个方向的连接。通过调用close(socket) API。表示Client不再会发送数据到Server端。(但Server还能继续发给Client端)。Client状态变为FIN_WAIT_1。</li>
<li>Server端收到FIN后，发送一个ACK报文给Client端(序号为M+1)。Server状态变为CLOSE_WAIT，Client收到序号为(M+1)的ACK后状态变为FIN_WAIT_2。Server端也发送一个FIN报文给Client端。(序号为N)<br>
表示Server也要终止到Client端这个方向的连接。通过调用close(socket) API。Server端状态变为LAST_ACK。</li>
<li>Client端收到报文FIN后，也发送一个ACK报文给服务器。(序号N+1)，Client状态变为TIME_WAIT。</li>
<li>Server端收到序号为(N+1)的ACK， Server的状态变为CLOSED。</li>
<li>等带2MSL之后，Client的状态也变为CLOSE。</li>
</ol>
<p>至此，一个完整的TCP连接就关闭了。</p>
<h2 id="1-52-有大量的TIME-WAIT状态怎么办？">1.52 有大量的TIME_WAIT状态怎么办？</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>time_wait 状态的影响</p>
<p>TCP 连接中，主动发起关闭连接的一端，会进入 time_wait 状态，time_wait 状态，默认会持续 2 MSL（报文的最大生存时间），一般是 2x2 mins，time_wait 状态下，TCP 连接占用的端口，无法被再次使用，TCP 端口数量，上限是 6.5w（65535，16 bit），<strong>大量 time_wait 状态存在，会导致新建 TCP 连接会出错，address already in use : connect异常</strong>。</p>
</li>
<li>
<p><strong>解决办法</strong></p>
<p>（1）<strong>客户端</strong>：HTTP 请求的头部，connection 设置为 keep-alive，保持存活一段时间：现在的浏览器，一般都这么进行了 。</p>
<p>（2）<strong>服务器端</strong></p>
<p>a. 允许 time_wait状态的 socket 被<strong>重用</strong></p>
<p>b. 缩减 time_wait 时间，设置为 1 MSL（即，2 mins）</p>
</li>
</ol>
<h2 id="1-53-请介绍socket通信的具体步骤。">1.53 请介绍socket通信的具体步骤。</h2>
<p><strong>参考回答</strong></p>
<p>sockets（套接字）编程有三种：<strong>流式套接字（SOCK_STREAM），数据报套接字（SOCK_DGRAM），原始套接字（SOCK_RAW）</strong>；基于TCP的socket编程是采用的流式套接字。</p>
<ol>
<li>
<p>服务器端编程的步骤</p>
<p>（1）加载套接字库，创建套接字(WSAStartup()/socket())；</p>
<p>（2）绑定套接字到一个IP地址和一个端口上(bind())；</p>
<p>（3）将套接字设置为监听模式等待连接请求(listen())；</p>
<p>（4）请求到来后，接受连接请求，返回一个新的对应于此次连接的套接字(accept())；</p>
<p>（5）用返回的套接字和客户端进行通信(send()/recv())；</p>
<p>（6）返回，等待另一连接请求；</p>
<p>（7）关闭套接字，关闭加载的套接字库(closesocket()/WSACleanup())。</p>
</li>
<li>
<p>客户端编程的步骤：</p>
<p>（1）加载套接字库，创建套接字(WSAStartup()/socket())；</p>
<p>（2）向服务器发出连接请求(connect())；</p>
<p>（3）和服务器端进行通信(send()/recv())；</p>
<p>（4）关闭套接字，关闭加载的套接字库(closesocket()/WSACleanup())。</p>
</li>
</ol>
<h2 id="1-54-服务端怎么提高处理socket连接的性能？">1.54 服务端怎么提高处理socket连接的性能？</h2>
<p><strong>参考回答</strong></p>
<p>提高处理socket连接的性能，请遵循以下技巧：</p>
<ol>
<li>最小化报文传输的延时。</li>
<li>最小化系统调用的负载。</li>
<li>为 Bandwidth Delay Product 调节 TCP 窗口。</li>
<li>动态优化 GNU/Linux TCP/IP 栈。</li>
</ol>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p>最小化报文传输的延时。</p>
<p>在通过 TCP socket 进行通信时，数据都拆分成了数据块，这样它们就可以封装到给定连接的 TCP payload（指 TCP 数据包中的有效负荷）中了。TCP payload 的大小取决于几个因素（例如最大报文长度和路径），但是这些因素在连接发起时都是已知的。为了达到最好的性能，我们的目标是使用尽可能多的可用数据来填充每个报文。当没有足够的数据来填充 payload 时（也称为最大报文段长度（maximum segment size）或 MSS），TCP 就会采用 Nagle 算法自动将一些小的缓冲区连接到一个报文段中。这样可以通过最小化所发送的报文的数量来提高应用程序的效率，并减轻整体的网络拥塞问题。</p>
</li>
<li>
<p>最小化系统调用的负载。</p>
<p>任何时候通过一个 socket 来读写数据时，都是在使用一个系统调用（system call）。这个调用（例如 read 或 write）跨越了用户空间应用程序与内核的边界。另外，在进入内核之前，该调用会通过 C 库来进入内核中的一个通用函数（system_call()）。从 system_call()中，这个调用会进入文件系统层，内核会在这儿确定正在处理的是哪种类型的设备。最后，调用会进入 socket 层，数据就是在这里进行读取或进行排队从而通过 socket 进行传输的（这涉及数据的副本）。</p>
<p>这个过程说明<strong>系统调用不仅仅是在应用程序和内核中进行操作的，而且还要经过应用程序和内核中的很多层次。<strong>这个过程耗费的资源很高，因此调用次数越多，通过这个调用链进行的工作所需要的时间就越长，应用程序的性能也就越低。由于我们</strong>无法避免这些系统调用</strong>，因此<strong>唯一的选择是最小化使用这些调用的次数</strong>。</p>
</li>
<li>
<p>为 Bandwidth Delay Product 调节 TCP 窗口。</p>
<p>TCP 的性能取决于几个方面的因素。两个最重要的因素是链接带宽（link bandwidth）（报文在网络上传输的速率）和 往返时间（round-trip time） 或 RTT（发送报文与接收到另一端的响应之间的延时）。这两个值确定了称为 Bandwidth Delay Product（BDP）的内容。</p>
<p>给定链接带宽和 RTT 之后，就可以计算出 BDP 的值了，不过这代表什么意义呢？BDP 给出了一种简单的方法来计算理论上最优的 TCP socket 缓冲区大小（其中保存了排队等待传输和等待应用程序接收的数据）。如果缓冲区太小，那么 TCP 窗口就不能完全打开，这会对性能造成限制。如果缓冲区太大，那么宝贵的内存资源就会造成浪费。如果设置的缓冲区大小正好合适，那么就可以完全利用可用的带宽。</p>
</li>
<li>
<p>动态优化 GNU/Linux TCP/IP 栈。</p>
<p>标准的 GNU/Linux 发行版试图对各种部署情况都进行优化。这意味着标准的发行版可能并没有对现有的环境进行特殊的优化。GNU/Linux 提供了很多可调节的内核参数，可以使用这些参数为自己的操作系统进行动态配置。</p>
</li>
</ol>
<h2 id="1-55-介绍一下流量控制和拥塞控制。">1.55 介绍一下流量控制和拥塞控制。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>流量控制和拥塞控制定义</p>
<p><strong>流量控制</strong></p>
<p>如果发送方把数据发送得过快，接收方可能会来不及接收，这就会造成数据的丢失。<strong>流量控制</strong>就是让发送方慢点，要让接收方来得及接收。</p>
<p><strong>拥塞控制</strong></p>
<p>拥塞控制就是防止过多的数据注入到网络中，这样可以使网络中的路由器或链路不致过载。</p>
</li>
<li>
<p>流量控制和拥塞控制区别</p>
<p>流量控制是<strong>端到端</strong>的控制，例如A通过网络给B发数据，A发送的太快导致B没法接收(B缓冲窗口过小或者处理过慢)，这时候的控制就是流量控制，原理是通过滑动窗口的大小改变来实现。</p>
<p>拥塞控制是A与B之间的网络发生堵塞导致传输过慢或者丢包，来不及传输。防止过多的数据注入到网络中，这样可以使网络中的路由器或链路不至于过载。拥塞控制是一个<strong>全局性</strong>的过程，涉及到所有的主机、路由器，以及与降低网络性能有关的所有因素。</p>
</li>
<li>
<p>TCP流量控制解决方法</p>
<p>TCP的流量控制是利用<strong>滑动窗口机制</strong>实现的，接收方在返回的数据中会包含自己的接收窗口的大小，以控制发送方的数据发送。</p>
</li>
<li>
<p>TCP拥塞控制解决方法</p>
<p>TCP拥塞控制的四种算法：<strong>慢开始、拥塞避免、快重传、快恢复。</strong></p>
<p>（1）**慢开始算法：**当主机开始发送数据时，并不清楚网络的负载情况，所以由小到大逐渐增大拥塞窗口，每经过一个传输轮次没有出现超时就将拥塞窗口加倍。同时还需要设置一个慢开始门限，在拥塞窗口小于慢开始门限时使用慢开始算法，大于慢开始门限时，使用拥塞避免算法；</p>
<p>（2）**拥塞避免算法：**在拥塞窗口大于慢开始门限时，让拥塞窗口按线性规律缓慢增长。即每经过一个传输轮次，拥塞窗口增大一个MSS最大报文段尺寸。（拥塞避免并非完全能够避免拥塞，只是使网络比较不容易出现拥塞）</p>
<p>（3）**快重传算法：**使发送方今早知道发生了个别报文段丢失，并不是出现网络拥塞。</p>
<p>要求接受不要登塞自己发送数据时才进行捎带确认，而是立即发送确认，即使收到了失序的报文段也要立即发出对已收到报文段的重复确认。而发送方一旦受到三个连续的重读确认，就将相应的报文段立即重传。</p>
<p>（4）**快恢复算法：**发送方知道只有个别报文段丢失而不是网络拥塞时，不启动慢开始算法，而是执行快恢复算法，将慢开始门限和拥塞窗口值调整为当前窗口的一半，开始执行拥塞避免算法</p>
</li>
</ol>
<h2 id="1-56-对路由协议是否有所了解？">1.56 对路由协议是否有所了解？</h2>
<p><strong>参考回答</strong></p>
<p>有了解。</p>
<ol>
<li>
<p>路由协议定义</p>
<p><strong>路由协议</strong>（英语：Routing protocol）是一种指定数据包转送方式的网上协议。Internet网络的主要节点设备是路由器，路由器通过路由表来转发接收到的数据。转发策略可以是人工指定的（通过静态路由、策略路由等方法）。在具有较小规模的网络中，人工指定转发策略没有任何问题。但是在具有较大规模的网络中（如跨国企业网络、ISP网络），如果通过人工指定转发策略，将会给网络管理员带来巨大的工作量，并且在管理、维护路由表上也变得十分困难。为了解决这个问题，动态路由协议应运而生。动态路由协议可以让路由器自动学习到其他路由器的网络，并且网络拓扑发生改变后自动更新路由表。网络管理员只需要配置动态路由协议即可，相比人工指定转发策略，工作量大大减少。</p>
</li>
<li>
<p>原理</p>
<p>路由协议通过在路由器之间共享路由信息来支持可路由协议。路由信息在相邻路由器之间传递，确保所有路由器知道到其它路由器的路径。总之，路由协议创建了路由表，描述了网络拓扑结构；路由协议与路由器，执行路由选择和数据包转发功能。</p>
</li>
<li>
<p>路由器的作用以及常见的路由协议</p>
<p>**路由协议主要运行于路由器上，路由协议是用来确定到达路径的，起到一个地图导航，负责找路的作用。**它工作在网络层。它包括RIP，IGRP（Cisco私有协议），EIGRP（Cisco私有协议），OSPF，IS-IS，BGP。以下为这六个协议的详细说明：</p>
<p>（1）RIP（路由信息协议）</p>
<p>RIP很早就被用在Internet上，是<strong>最简单的路由协议</strong>。它是“路由信息协议（Route Information Protocol）”的简写，主要传递路由信息，通过每隔30秒广播一次路由表，维护相邻路由器的位置关系，同时根据收到的路由表信息计算自己的路由表信息。RIP是一个<strong>距离矢量路由协议</strong>，最大跳数为15跳，超过15跳的网络则认为目标网络不可达。此协议通常用在网络架构较为简单的小型网络环境。分为RIPv1和RIPv2两个版本，后者支持VLSM技术以及一系列技术上的改进。RIP的收敛速度较慢。</p>
<p>（2）IGRP（内部网关路由协议）</p>
<p>IGRP协议是“内部网关路由协议（Interior Gateway Routing Protocol）”的缩写，由Cisco于二十世纪八十年代独立开发，属于Cisco私有协议。IGRP和RIP一样，同属距离矢量路由协议，因此在诸多方面有着相似点，如IGRP也是周期性的广播路由表，也存在最大跳数（默认为100跳，达到或超过100跳则认为目标网络不可达）。IGRP最大的特点是使用了混合度量值，同时考虑了链路的带宽、延迟、负载、MTU、可靠性5个方面来计算路由的度量值，而不像其他IGP协议单纯的考虑某一个方面来计算度量值。IGRP已经被Cisco独立开发的EIGRP协议所取代，版本号为12.3及其以上的Cisco IOS（Internetwork Operating System）已经不支持该协议，已经罕有运行IGRP协议的网络。</p>
<p>（3）EIGRP（增强型内部网关路由协议）</p>
<p>由于IGRP协议的种种缺陷以及不足，Cisco开发了EIGRP协议（增强型内部网关路由协议）来取代IGRP协议。EIGRP属于高级距离矢量路由协议（又称混合型路由协议），继承了IGRP的混合度量值，最大特点在于引入了非等价负载均衡技术，并拥有极快的收敛速度。EIGRP协议在Cisco设备网络环境中广泛部署。</p>
<p>（4）OSPF（开放式最短路径优先）</p>
<p>OSPF协议是“开放式最短路径优先（Open Shortest Path First）”的缩写，属于链路状态路由协议。OSPF提出了“区域（area）”的概念，每个区域中所有路由器维护着一个相同的链路状态数据库（LSDB）。区域又分为骨干区域（骨干区域的编号必须为0）和非骨干区域（非0编号区域），如果一个运行OSPF的网络只存在单一区域，则该区域可以是骨干区域或者非骨干区域。如果该网络存在多个区域，那么必须存在骨干区域，并且所有非骨干区域必须和骨干区域直接相连。OSPF利用所维护的链路状态数据库，通过最短路径优先算法（SPF算法）计算得到路由表。OSPF的收敛速度较快。由于其特有的开放性以及良好的扩展性，OSPF协议在各种网络中广泛部署。</p>
<p>（5）IS-IS（中间系统到中间系统）</p>
<p>IS-IS协议是Intermediate system to intermediate system（中间系统到中间系统）的缩写，属于链路状态路由协议。标准IS-IS协议是由国际标准化组织制定的ISO/IEC 10589:2002所定义的，标准IS-IS不适合用于IP网络，因此IETF制定了适用于IP网络的集成化IS-IS协议（Integrated IS-IS）。和OSPF相同，IS-IS也使用了“区域”的概念，同样也维护着一份链路状态数据库，通过最短生成树算法（SPF）计算出最佳路径。IS-IS的收敛速度较快。集成化IS-IS协议是ISP骨干网上最常用的IGP协议。</p>
<p>（6）BGP（边界网关协议）</p>
<p>为了维护各个ISP的独立利益，标准化组织制定了ISP间的路由协议BGP。BGP是“边界网关协议（Border Gateway Protocol）”的缩写，处理各ISP之间的路由传递。但是BGP运行在相对核心的地位，需要用户对网络的结构有相当的了解，否则可能会造成较大损失。</p>
</li>
</ol>
<h2 id="1-57-视频直播可能需要使用到什么样的协议？">1.57 视频直播可能需要使用到什么样的协议？</h2>
<p><strong>参考回答</strong></p>
<p>视频直播有多种协议，使用rtmp协议的就是rtmp直播。直播流就是视频流，即传递的视频数据。常见的协议有<strong>RTMP、RTSP、HTTP协议</strong>，这三个协议都属于互联网<strong>TCP/IP五层体系结构中应用层</strong>的协议。理论上这三种都可以用来做视频直播或点播。但通常来说，**直播一般用RTMP、RTSP，而点播用HTTP。**下面分别介绍下三者的特点。</p>
<ol>
<li>
<p>RTMP协议</p>
<p>（1）是流媒体协议；</p>
<p>（2）RTMP协议是Adobe的私有协议，未完全公开；</p>
<p>（3）RTMP协议一般传输的是flv，f4v格式流；</p>
<p>（4）RTMP一般在TCP1个通道上传输命令和数据。</p>
</li>
<li>
<p>RTSP协议</p>
<p>（1）是流媒体协议；</p>
<p>（2）RTSP协议是共有协议，并有专门机构做维护；</p>
<p>（3）RTSP协议一般传输的是ts、mp4格式的流；</p>
<p>（4）RTSP传输一般需要2-3个通道，命令和数据通道分离。</p>
</li>
<li>
<p>HTTP协议</p>
<p>（1）不是是流媒体协议；</p>
<p>（2）HTTP协议是共有协议，并有专门机构做维护；</p>
<p>（3）HTTP协议没有特定的传输流；</p>
<p>（4）HTTP传输一般需要2-3个通道，命令和数据通道分离。</p>
</li>
</ol>
<p><strong>答案解析</strong></p>
<p><strong>扩展资料</strong></p>
<p>一个完整的视频直播过程，包括<strong>采集、处理、编码、封装、推流、传输、转码、分发、解码、播放等</strong>。</p>
<ol>
<li>
<p>采集</p>
<p>音频采集音频的采集过程主要通过设备将环境中的模拟信号采集成 PCM 编码的原始数据，然后编码压缩成 MP3 等格式的数据分发出去。常见的音频压缩格式有：MP3，AAC，HE-AAC，Opus，FLAC，Vorbis (Ogg)，Speex 和 AMR等。</p>
<p>图像采集 图像的采集过程主要由摄像头等设备拍摄成 YUV 编码的原始数据，然后经过编码压缩成 H.264 等格式的数据分发出去。常见的视频封装格式有：MP4、3GP、AVI、MKV、WMV、MPG、VOB、FLV、SWF、MOV、RMVB 和 WebM 等。</p>
</li>
<li>
<p>处理</p>
<p>视频或者音频完成采集之后得到原始数据，为了增强一些现场效果或者加上一些额外的效果，我们一般会在将其编码压缩前进行处理。</p>
<p>视频：美颜、水印、路径、自定义。</p>
<p>音频：混音、降噪、特效、自定义。</p>
</li>
<li>
<p>编码</p>
<p>对流媒体传输来说，编码非常重要，它的编码性能、编码速度和编码压缩比会直接影响整个流媒体传输的用户体验和传输成本。</p>
<p>常见的视频编码器：</p>
<p>（1）H.264/AVC</p>
<p>（2）HEVC/H.265</p>
<p>（3）VP8</p>
<p>（4）VP9</p>
<p>（5）FFmpeg</p>
<p>音频编码器：Mp3, AAC等。</p>
</li>
<li>
<p>封装</p>
<p>把编码器生成的多媒体内容(视频，音频，字幕，章节信息等)混合封装在一起几种常见的封装格式：</p>
<p>（1）AVI 格式(后缀为 .avi)</p>
<p>（2）DV-AVI 格式(后缀为 .avi)</p>
<p>（3）QuickTime File Format 格式(后缀为 .mov)</p>
<p>（4）MPEG 格式(文件后缀可以是 .mpg .mpeg .mpe .dat .vob .asf .3gp .mp4等)</p>
<p>（5）WMV 格式(后缀为.wmv .asf)</p>
<p>（6）Real Video 格式(后缀为 .rm .rmvb)</p>
<p>（7）Flash Video 格式(后缀为 .flv)</p>
<p>（8）Matroska 格式(后缀为 .mkv)</p>
<p>（9）MPEG2-TS 格式 (后缀为 .ts)</p>
<p>目前，我们在流媒体传输，尤其是直播中主要采用的就是 FLV 和 MPEG2-TS 格式，分别用于 RTMP/HTTP-FLV 和 HLS 协议。</p>
</li>
<li>
<p>推流</p>
<p>推流是指使用推流工具等内容抓取软件把直播内容传输到服务器的过程。推送协议主要有三种：</p>
<p>（1）RTSP(Real Time Streaming Protocol)：实时流传送协议，是用来控制声音或影像的多媒体串流协议, 由Real Networks和Netscape共同提出的;</p>
<p>（2）RTMP(Real Time Messaging Protocol)：实时消息传送协议，是Adobe公司为Flash播放器和服务器之间音频、视频和数据传输 开发的开放协议;</p>
<p>（3）HLS(HTTP Live Streaming)：是苹果公司(Apple Inc.)实现的基于HTTP的流媒体传输协议;RTMP是目前主流的流媒体传输协议，广泛用于直播领域，市面上绝大多数的直播产品都采用了这个协议。</p>
<p>RTMP协议基于 TCP，是一种设计用来进行实时数据通信的网络协议，主要用来在 flash/AIR 平台和支持 RTMP 协议的流媒体/交互服务器之间进行音视频和数据通信。支持该协议的软件包括 Adobe Media Server/Ultrant Media Server/red5 等。它有三种变种：</p>
<p>（1）RTMP工作在TCP之上的明文协议，使用端口1935；</p>
<p>（2）RTMPT封装在HTTP请求之中，可穿越防火墙;</p>
<p>（3）RTMPS类似RTMPT，但使用的是HTTPS连接;</p>
<p>RTMP协议就像一个用来装数据包的容器，这些数据可以是AMF格式的数据,也可以是FLV中的视/音频数据。一个单一的连接可以通过不同的通道传输多路网络流。这些通道中的包都是按照固定大小的包传输的。</p>
</li>
<li>
<p>传输</p>
<p>推送出去的流媒体需要传输到观众，整个链路就是传输网络。</p>
</li>
<li>
<p>转码</p>
<p>视频直播播流端的码率是根据推流端决定的，即播流端的码率是与推流端的码率一致的。但是遇到以下场景会造成直播效果较差：推流端码率与播流端带宽不相匹配。当推流端码率较高而客户端带宽资源有限就会导致播放出现卡顿，而当推流端码率较低但是客户端对于直播效率要求较高时会导致播放效果较差。播放器插件需要实现多码率切换。前端播放器插件常可以设置码率切换，这就需要同一路推流可以同时提供多种码率的播流地址。因此，视频直播提供了实时转码功能对同一路推流地址同时提供多路不同码率播流地址提供服务。</p>
</li>
<li>
<p>分发</p>
<p>流媒体服务器的作用是负责直播流的发布和转播分发功能。</p>
</li>
<li>
<p>解码</p>
<p>编码器(Encoder)：压缩信号的设备或程序；</p>
<p>解码器(Decoder)：解压缩信号的设备或程序；</p>
<p>编解码器(Codec)：编解码器对。</p>
</li>
<li>
<p>播放器流播放主要是实现直播节目在终端上的展现。因为这里使用的传输协议是RTMP， 所以只要支持 RTMP 流协议的播放器都可以使用。</p>
</li>
</ol>
<h2 id="1-58-谈谈单工、双工、半双工的通信方式。">1.58 谈谈单工、双工、半双工的通信方式。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>**单工：**数据传输只支持数据在一个方向上传输；在同一时间只有一方能接受或发送信息，不能实现双向通信。举例：电视，广播。</li>
<li>**半双工：**半双工数据传输允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信；在同一时间只可以有一方接受或发送信息，可以实现双向通信。举例：对讲机。</li>
<li>**双工：**全双工数据通信允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力；在同一时间可以同时接受和发送信息，实现双向通信。举例：电话通信。</li>
</ol>
<p><strong>答案解析</strong></p>
<p><strong>扩展资料：</strong></p>
<p>单工、半双工和全双工是电信计算机网络中的三种通信信道。这些通信信道可以提供信息传达的途径。通信信道可以是物理传输介质或通过多路复用介质的逻辑连接。物理传输介质是指能够传播能量波的材料物质，例如数据通信中的导线。并且逻辑连接通常指电路交换连接或分组模式虚拟电路连接，例如无线电信通道。由于通信信道的帮助，信息可以无障碍地传输。</p>
<p>单工模式一般用在只向一个方向传输数据的场合。例如计算机与打印机之间的通信是单工模式，因为只有计算机向打印机传输数据，而没有相反方向的数据传输。还有在某些通信信道中，如单工无线发送等。</p>
<h2 id="1-59-说一说内网和外网通信的过程。">1.59 说一说内网和外网通信的过程。</h2>
<p><strong>参考回答</strong></p>
<ol>
<li>
<p>公有IP和私有IP概念</p>
<p><strong>公有地址(Public address)</strong>：由 Inter NIC(Internet Network Information Center 因特网信息中心)负责。这些 IP 地址分配给注册并向Inter NIC提出申请的组织机构，公有 IP 全球唯一，通过它直接访问因特网(直接能上网)。</p>
<p><strong>私有地址(Private address)</strong>：属于非注册地址，专门为组织机构内部使用，说白了，私有 IP 不能直接上网。</p>
<p>家里路由器分出来的IP就是私有IP（局域网IP），真正拥有公有IP的是运营商（电信、移动、联通），运营商购买了一些公有IP（外网IP），然后通过这些公有IP分出来，再分给一个一个的用户使用。所以，<strong>A 家庭的局域网 IP 和 B 家庭的局域网 IP 相同很正常</strong>，但是，最终 A 和 B 能上网(数据走出去)还是通过运营商的公有 IP，毕竟，公有 IP 的资源有限，这一片区域的用户使用的很有可能(实际上就是这样的)是同一个公有 IP。</p>
</li>
<li>
<p>内网和外网通信过程</p>
<p>内网和外网的通信是通过<strong>端口映射</strong>来实现的。</p>
<p>我们平时经过路由器，通过宽带，最终去到运营商那边，数据是从运营商出去，最终数据是回到运营商那边，运营商再把数据发送到用户的电脑。路由器，至少有两个端口：WAN 口和 LAN 口。<strong>WAN口</strong>接外部 IP 地址用，通常指的是出口，转发来自内部 LAN 接口的 IP 数据包，这个口的 IP 是唯一的。<strong>LAN口</strong>接内部 IP 地址用，LAN 内部是交换机。</p>
<p>**举例：**假如 A 和 B 的局域网（内网） IP 相同（192.168.31.11），当他们同时访问百度服务器（外网）的时候，百度服务器如何区分哪个是 A，哪个是 B 呢？</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/AB%E8%AE%BF%E9%97%AE%E7%99%BE%E5%BA%A6%E6%9C%8D%E5%8A%A1%E5%99%A8.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="AB访问百度服务器">
</li>
</ol>
 <img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/AB%E8%AE%BF%E9%97%AE%E7%99%BE%E5%BA%A6%E6%9C%8D%E5%8A%A1%E5%99%A82.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="AB访问百度服务器2">
<p>**分析：**为了方便大家理解，我们把 IP 的转化方向反过来分析（准确来说，公网转局域网）。A 电脑的 IP 是局域网 IP（192.168.31.11），这个 IP（192.168.31.11）是从路由器的 lan口分配的。</p>
<p>当我们上百度的时候，经过路由器的 wan口，进行相应的IP、端口转化：192.168.31.11:80 -&gt; 10.221.0.24:8080,所以，从 wan口出去的地址为：10.221.0.24:8080。如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/AB%E8%AE%BF%E9%97%AE%E7%99%BE%E5%BA%A6%E6%9C%8D%E5%8A%A1%E5%99%A83.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="AB访问百度服务器3">
<p>最后，经过运营商，运营商那边会做相应的端口映射（而且是动态端口映射），子网 IP（10.221.0.24:8080）转化为公网 IP（128.0.0.1:8888），通过这个公网 IP 去访问百度服务器。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/AB%E8%AE%BF%E9%97%AE%E7%99%BE%E5%BA%A6%E6%9C%8D%E5%8A%A1%E5%99%A84.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="AB访问百度服务器4">
<p>同理，B 的过程也是一样。通过这样的层层端口映射，最终保证地址（IP + 端口）的唯一性。A 和 B 访问百度服务器，尽管它们的局域网 IP 是一样的，但是最终它们访问百度的地址（IP + 端口）是唯一的，所以，百度服务器回复时，原路返回时能够区分到底给谁回。</p>
<p><strong>答案解析</strong></p>
<ol>
<li>
<p>概念</p>
<p><strong>内网（局域网）</strong></p>
<p>指在某一区域内由多台计算机互联成的计算机组。一般是方圆几千米以内。局域网可以实现文件管理、应用软件共享、打印机共享、工作组内的日程安排、电子邮件和传真通信服务等功能。局域网是封闭型的，可以由办公室内的两台计算机组成，也可以由一个公司内的上千台计算机组成。</p>
<p><strong>外网（广域网）</strong></p>
<p>又称<strong>公网</strong>。是连接不同地区局域网或城域网计算机通信的远程网。通常跨接很大的物理范围，所覆盖的范围从几十公里到几千公里，它能连接多个地区、城市和国家，或横跨几个洲并能提供远距离通信，形成国际性的远程网络。广域网并不等同于互联网。</p>
</li>
<li>
<p>内网和外网的区别</p>
<p>（1）ip地址设置的区别，一般内网有自己的IP号段，也不会和互联网号段冲突，内网就是从路由器以下开始的，而且IP都是以192开头的IP。一般是不能拥有外网IP的，因为个人或者小群体用外网也是一种资源浪费，所以一般都是通过内网去上网的，外网Ip一般都是用于公司企业，学校等机构的。</p>
<p>（2）内网电脑连接外网需要一个统一出口，可能被限制一些不必要的访问，而外网就不经路由器或交换机就可以上网的网络，可以直接被外界所访问到，无需经如何设备，直接连接电脑。</p>
<p>（3）内网相对外网会多一层安全防火墙（外网路由），相对来说抵御来自外网的攻击能力会好一些；内网不足之处在于，可能会遭到来自内部的攻击；因为要共享带宽，相对网速可能会慢些（终端越多越慢）。</p>
<p>（4）内网的ip可以经常换，可以自己定义规则；而外网的ip一般都是固定的，你装好宽带的时候，你的ip就固定下来了。</p>
</li>
</ol>
<h1>3. MySQL</h1>
<h2 id="1-索引">1. 索引</h2>
<h2 id="1-1-说一说你对MySQL索引的理解">1.1 说一说你对MySQL索引的理解</h2>
<p>索引是一个单独的、存储在磁盘上的数据库结构，包含着对数据表里所有记录的引用指针。使用索引可以快速找出在某个或多个列中有一特定值的行，所有MySQL列类型都可以被索引，对相关列使用索引是提高查询操作速度的最佳途径。</p>
<p><strong>索引是在存储引擎中实现的</strong>，因此，每种存储引擎的索引都不一定完全相同，并且每种存储引擎也不一定支持所有索引类型。<strong>MySQL中索引的存储类型有两种，即BTREE和HASH</strong>，具体和表的存储引擎相关。<strong>MyISAM和InnoDB存储引擎只支持BTREE索引</strong>；MEMORY/HEAP存储引擎可以支持HASH和BTREE索引。</p>
<p><strong>索引的优点</strong>主要有以下几条：</p>
<ol>
<li>通过创建<strong>唯一索引</strong>，可以保证数据库表中每一行数据的唯一性。****</li>
<li>可以大大<strong>加快数据的查询速度</strong>，这也是创建索引的主要原因。</li>
<li>在实现数据的参考完整性方面，可以加速表和表之间的连接。</li>
<li>在使用分组和排序子句进行数据查询时，也可以显著减少查询中分组和排序的时间。</li>
</ol>
<p>索引的缺点：增加索引也有许多不利的方面，主要表现在如下几个方面：</p>
<ol>
<li>创建索引和维护索引要<strong>耗费时间</strong>，并且随着数据量的增加所耗费的时间也会增加。</li>
<li>索引需要<strong>占磁盘空间</strong>，除了数据表占数据空间之外，每一个索引还要占一定的物理空间，如果有大量的索引，索引文件可能比数据文件更快达到最大文件尺寸。</li>
<li>当对表中的数据进行增加、删除和修改的时候，索引也要<strong>动态地维护</strong>，这样就<strong>降低了数据的维护速度</strong>。</li>
</ol>
<h2 id="1-2-索引有哪几种？">1.2 索引有哪几种？</h2>
<ol>
<li>
<p>普通索引和唯一索引</p>
<p>普通索引是MySQL中的基本索引类型，允许在定义索引的列中插入重复值和空值。</p>
<p>唯一索引要求索引列的值必须唯一，但允许有空值。如果是组合索引，则列值的组合必须唯一。</p>
<p>主键索引是一种特殊的唯一索引，不允许有空值。</p>
</li>
<li>
<p>单列索引和组合索引</p>
<p>单列索引即一个索引只包含单个列，一个表可以有多个单列索引。</p>
<p>组合索引是指在表的多个字段组合上创建的索引，只有在查询条件中使用了这些字段的左边字段时，索引才会被使用。使用组合索引时遵循最左前缀集合。</p>
</li>
<li>
<p>全文索引</p>
<p>全文索引类型为FULLTEXT，在定义索引的列上支持值的全文查找，允许在这些索引列中插入重复值和空值。全文索引可以在CHAR、VARCHAR或者TEXT类型的列上创建。MySQL中只有MyISAM存储引擎支持全文索引。</p>
</li>
<li>
<p>空间索引</p>
<p>空间索引是对空间数据类型的字段建立的索引，MySQL中的空间数据类型有4种，分别是GEOMETRY、POINT、LINESTRING和POLYGON。MySQL使用SPATIAL关键字进行扩展，使得能够用创建正规索引类似的语法创建空间索引。创建空间索引的列，必须将其声明为NOT NULL，空间索引只能在存储引擎为MyISAM的表中创建。</p>
</li>
</ol>
<h2 id="1-3-MySQL怎么判断要不要加索引？">1.3 MySQL怎么判断要不要加索引？</h2>
<p>建议按照如下的原则来创建索引：</p>
<ol>
<li>当唯一性是某种数据本身的特征时，指定唯一索引。使用唯一索引需能确保定义的列的数据完整性，以提高查询速度。</li>
<li>在频繁进行排序或分组（即进行group by或order by操作）的列上建立索引，如果待排序的列有多个，可以在这些列上建立组合索引。</li>
</ol>
<h2 id="1-4-只要创建了索引，就一定会走索引吗？">1.4 只要创建了索引，就一定会走索引吗？</h2>
<p>不一定。</p>
<p>比如，在<strong>使用组合索引</strong>的时候，如果没<strong>有遵从“最左前缀”的原则</strong>进行搜索，则索引是不起作用的。</p>
<p>举例，假设在id、name、age字段上已经成功建立了一个名为MultiIdx的组合索引。索引行中按id、name、age的顺序存放，索引可以搜索id、（id,name）、（id, name, age）字段组合。如果列不构成索引最左面的前缀，那么MySQL不能使用局部索引，如（age）或者（name,age）组合则不能使用该索引查询。</p>
<h2 id="1-5-如何判断数据库的索引有没有生效？">1.5 如何判断数据库的索引有没有生效？</h2>
<p>可以使用EXPLAIN语句查看索引是否正在使用。</p>
<p>举例，假设已经创建了book表，并已经在其year_publication字段上建立了普通索引。执行如下语句：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">EXPLAIN SELECT * FROM book WHERE year_publication=1990;<br></code></pre></td></tr></table></figure>
<p>EXPLAIN语句将为我们输出详细的SQL执行信息，其中：</p>
<ul>
<li><strong>possible_keys</strong>行给出了MySQL在搜索数据记录时可选用的各个索引。</li>
<li><strong>key</strong>行是MySQL实际选用的索引。</li>
</ul>
<p>如果possible_keys行和key行都包含year_publication字段，则说明在查询时使用了该索引。</p>
<h2 id="1-5-如何评估一个索引创建的是否合理？（如何设计索引）">1.5 如何评估一个索引创建的是否合理？（如何设计索引）</h2>
<p>建议按照如下的原则来设计索引：</p>
<ol>
<li>避免对经常更新的表进行过多的索引，并且索引中的列要尽可能少。应该经常用于查询的字段创建索引，但要避免添加不必要的字段。</li>
<li>数据量小的表最好不要使用索引，由于数据较少，查询花费的时间可能比遍历索引的时间还要短，索引可能不会产生优化效果。</li>
<li>在条件表达式中经常用到的不同值较多的列上建立索引，在不同值很少的列上不要建立索引。比如在学生表的“性别”字段上只有“男”与“女”两个不同值，因此就无须建立索引，如果建立索引不但不会提高查询效率，反而会严重降低数据更新速度。</li>
<li>当唯一性是某种数据本身的特征时，指定唯一索引。使用唯一索引需能确保定义的列的数据完整性，以提高查询速度。</li>
<li>在频繁进行排序或分组（即进行group by或order by操作）的列上建立索引，如果待排序的列有多个，可以在这些列上建立组合索引。</li>
</ol>
<h2 id="1-6-索引是越多越好吗？">1.6 索引是越多越好吗？</h2>
<p>索引并非越多越好，一个表中如有大量的索引，不仅占用磁盘空间，还会影响INSERT、DELETE、UPDATE等语句的性能，因为在表中的数据更改时，索引也会进行调整和更新。</p>
<h2 id="1-7-数据库索引失效了怎么办？">1.7 数据库索引失效了怎么办？</h2>
<p>可以采用以下几种方式，来避免索引失效：</p>
<ol>
<li>使用组合索引时，需要遵循“最左前缀”原则；</li>
<li>不在索引列上做任何操作，例如计算、函数、类型转换，会导致索引失效而转向全表扫描；</li>
<li>尽量使用覆盖索引（之访问索引列的查询），减少 <code>select *</code> 覆盖索引能减少回表次数；</li>
<li>MySQL在使用不等于（!=或者&lt;&gt;）的时候无法使用索引会导致全表扫描；</li>
<li>LIKE以通配符开头（%abc）MySQL索引会失效变成全表扫描的操作；</li>
<li>字符串不加单引号会导致索引失效（可能发生了索引列的隐式转换）；</li>
<li>少用or，用它来连接时会索引失效。</li>
</ol>
<h2 id="1-8-所有的字段都适合创建索引吗？">1.8 所有的字段都适合创建索引吗？</h2>
<p>不是。</p>
<p>下列几种情况，是不适合创建索引的：</p>
<ol>
<li><strong>频繁更新的字段</strong>不适合建立索引；</li>
<li><strong>where条件中用不到的字段</strong>不适合建立索引；</li>
<li><strong>数据比较少的表</strong>不需要建索引；</li>
<li><strong>数据重复且分布比较均匀的的字段</strong>不适合建索引，例如性别、真假值；</li>
<li><strong>参与列计算的列</strong>不适合建索引。</li>
</ol>
<h2 id="1-9-说一说索引的实现原理">1.9 说一说索引的实现原理</h2>
<p>在MySQL中，索引是在存储引擎层实现的，不同存储引擎对索引的实现方式是不同的，下面我们探讨一下<strong>MyISAM</strong>和<strong>InnoDB</strong>两个存储引擎的索引实现方式。</p>
<h3 id="MyISAM索引实现：">MyISAM索引实现：</h3>
<p>MyISAM引擎使用<strong>B+Tree作为索引结构</strong>，<strong>叶节点的data域存放的是数据记录的地址</strong>，MyISAM索引的原理图如下。这里假设表一共有三列，假设我们以Col1为主键，则上图是一个MyISAM表的主索引（Primary key）示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。在MyISAM中，主索引和辅助索引（Secondary key）在结构上没有任何区别，只是主索引要求key是唯一的，而辅助索引的key可以重复。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/index-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>如果我们在Col2上建立一个辅助索引，则此索引的结构如下图所示。同样也是一颗B+Tree，data域保存数据记录的地址。因此，MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引，如果指定的Key存在，则取出其data域的值，然后以data域的值为地址，读取相应数据记录。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/index-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h3 id="InnoDB索引实现：">InnoDB索引实现：</h3>
<p>虽然InnoDB也使用<strong>B+Tree作为索引结构</strong>，但具体实现方式却与MyISAM截然不同。</p>
<p>第一个重大区别是<strong>InnoDB的数据文件本身就是索引文件</strong>。</p>
<p>从上文知道，<strong>MyISAM索引文件和数据文件是分离的</strong>，<strong>索引文件仅保存数据记录的地址</strong>。</p>
<p>而在InnoDB中，<strong>表数据文件本身就是按B+Tree组织的一个索引结构</strong>，这棵树的<strong>叶节点data域保存了完整的数据记录</strong>。这个索引的key是数据表的主键，因此InnoDB表数据文件本身就是主索引。</p>
<p>下图是InnoDB主索引（同时也是数据文件）的示意图，可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集，所以InnoDB要求表必须有主键（MyISAM可以没有），如果没有显式指定，则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键，如果不存在这种列，则MySQL自动为InnoDB表生成一个隐含字段作为主键，这个字段长度为6个字节，类型为长整形。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/index-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p><strong>第二个与MyISAM索引的不同</strong>是<strong>InnoDB的辅助索引data域存储相应记录主键的值而不是地址</strong>。</p>
<p>换句话说，<strong>InnoDB的所有辅助索引都引用主键作为data域</strong>。下图为定义在Col3上的一个辅助索引。这里以英文字符的ASCII码作为比较准则。聚集索引这种实现方式使得按主键的搜索十分高效，但是<strong>辅助索引搜索需要检索两遍索引：首先检索辅助索引获得主键，然后用主键到主索引中检索获得记录。</strong></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/index-4.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助，例如知道了InnoDB的索引实现后，就很容易明白为什么<strong>不建议使用过长的字段作为主键</strong>，因为所有辅助索引都引用主索引，过长的主索引会令辅助索引变得过大。再例如，<strong>用非单调的字段作为主键在InnoDB中不是个好主意</strong>，<strong>因为InnoDB数据文件本身是一颗B+Tree，非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整，十分低效，而使用自增字段作为主键则是一个很好的选择。</strong></p>
<h2 id="1-10-介绍一下数据库索引的重构过程">1.10 介绍一下数据库索引的重构过程</h2>
<h3 id="什么时候需要重建索引呢？">什么时候需要重建索引呢？</h3>
<ol>
<li>表上频繁发生update,delete操作；</li>
<li>表上发生了alter table …move操作（move操作导致了rowid变化）。</li>
</ol>
<h3 id="怎么判断索引是否应该重建？">怎么判断索引是否应该重建？</h3>
<ol>
<li>
<p>一般看索引是否倾斜的严重，是否浪费了空间，对索引进行结构分析：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql">analyze index index_name validate structure;<br></code></pre></td></tr></table></figure>
</li>
<li>
<p>在相同的session中查询index_stats表：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">select</span> height,DEL_LF_ROWS<span class="hljs-operator">/</span>LF_ROWS <span class="hljs-keyword">from</span> index_stats;<br></code></pre></td></tr></table></figure>
<p>当查询的height&gt;=4（索引的深度，即从根到叶节点的高度）或DEL_LF_ROWS/LF_ROWS&gt;0.2的情况下，就应该考虑重建该索引。</p>
</li>
</ol>
<h3 id="如何重建索引？">如何重建索引？</h3>
<ul>
<li>
<p>drop原索引，然后再创建索引：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">drop</span> index index_name;<br><span class="hljs-keyword">create</span> index index_name <span class="hljs-keyword">on</span> table_name (index_column);<br></code></pre></td></tr></table></figure>
<p>这种方式相当耗时，一般不建议使用。</p>
</li>
<li>
<p>直接重建索引：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">alter</span> index indexname rebuild;<br><span class="hljs-keyword">alter</span> index indexname rebuild online;<br></code></pre></td></tr></table></figure>
<p>此方法较快，建议使用。</p>
</li>
</ul>
<p>rebuild是快速重建索引的一种有效的办法，因为它是一种使用现有索引项来重建新索引的方法。如果重建索引时有其他用户在对这个表操作，尽量使用带online参数来最大限度的减少索引重建时将会出现的任何加锁问题。由于新旧索引在建立时同时存在，因此，使用这种重建方法需要有额外的磁盘空间可供临时使用，当索引建完后把老索引删除，如果没有成功，也不会影响原来的索引。利用这种办法可以用来将一个索引移到新的表空间。</p>
<h3 id="rebuild重建索引的过程：">rebuild重建索引的过程：</h3>
<ol>
<li>Rebuild以index fast full scan或table full scan方式（采用那种方式取决于cost）读取原索引中的数据来构建一个新的索引，重建过程中有排序操作，rebuild online执行表扫描获取数据，重建过程中有排序的操作；</li>
<li>Rebuild会阻塞DML操作，rebuild online不会阻塞DML操作；</li>
<li>rebuild online时系统会产生一个SYS_JOURNAL_xxx的IOT类型的系统临时日志表，所有rebuild online时索引的变化都记录在这个表中，当新的索引创建完成后，把这个表的记录维护到新的索引中去，然后drop掉旧的索引，rebuild online就完成了。</li>
</ol>
<h3 id="重建索引过程中的注意事项：">重建索引过程中的注意事项：</h3>
<ol>
<li>执行rebuild操作时，需要检查表空间是否足够；</li>
<li>虽然说rebuild online操作允许DML操作，但还是建议在业务不繁忙时间段进行；</li>
<li>Rebuild操作会产生大量Redo Log；</li>
</ol>
<h2 id="1-11-MySQL的索引为什么用B-树？">1.11 MySQL的索引为什么用B+树？</h2>
<p>B+树由B树和索引顺序访问方法演化而来，它是为磁盘或其他直接存取辅助设备设计的一种平衡查找树，在B+树中，所有记录节点都是按键值的大小顺序存放在同一层的叶子节点，各叶子节点通过指针进行链接。如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/btree-1.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>B+树索引在数据库中的一个特点就是高扇出性，例如在InnoDB存储引擎中，每个页的大小为16KB。在数据库中，B+树的高度一般都在2～4层，这意味着查找某一键值最多只需要2到4次IO操作，这还不错。因为现在一般的磁盘每秒至少可以做100次IO操作，2～4次的IO操作意味着查询时间只需0.02～0.04秒。</p>
<h2 id="1-12-联合索引的存储结构是什么，它的有效方式是什么？-什么是联合索引">1.12 联合索引的存储结构是什么，它的有效方式是什么？(什么是联合索引)</h2>
<p>从本质上来说，联合索引还是一棵B+树，不同的是联合索引的键值数量不是1，而是大于等于2，参考下图。另外，只有在查询条件中使用了这些字段的左边字段时，索引才会被使用，所以使用联合索引时遵循最左前缀集合。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/index.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h2 id="1-13-MySQL的Hash索引和B树索引有什么区别？">1.13 MySQL的Hash索引和B树索引有什么区别？</h2>
<p><strong>hash索引底层就是hash表</strong>，进行查找时，调用一次hash函数就可以获取到相应的键值，之后进行回表查询获得实际数据。</p>
<p><strong>B+树底层实现是多路平衡查找树</strong>，对于每一次的查询都是从根节点出发，查找到叶子节点方可以获得所查键值，然后根据查询判断是否需要回表查询数据。</p>
<h3 id="不同：">不同：</h3>
<ul>
<li>**hash索引进行等值查询更快(一般情况下)，但是却无法进行范围查询。**因为在hash索引中经过hash函数建立索引之后，索引的顺序与原顺序无法保持一致，不能支持范围查询。而B+树的的所有节点皆遵循(左节点小于父节点，右节点大于父节点，多叉树也类似)，天然支持范围。</li>
<li><strong>hash索引不支持使用索引进行排序</strong>，原理同上。</li>
<li><strong>hash索引不支持模糊查询以及多列索引的最左前缀匹配</strong>，原理也是因为hash函数的不可预测。</li>
<li><strong>hash索引任何时候都避免不了回表查询数据，而B+树在符合某些条件(聚簇索引，覆盖索引等)的时候可以只通过索引完成查询。</strong></li>
<li><strong>hash索引虽然在等值查询上较快，但是不稳定，性能不可预测</strong>，当某个键值存在大量重复的时候，发生hash碰撞，此时效率可能极差。而<strong>B+树的查询效率比较稳定</strong>，对于所有的查询都是从根节点到叶子节点，且树的高度较低。</li>
</ul>
<p>因此，在大多数情况下，直接选择B+树索引可以获得稳定且较好的查询速度。而不需要使用hash索引。</p>
<h2 id="1-14-聚簇索引和非聚簇索引有什么区别？">1.14 聚簇索引和非聚簇索引有什么区别？</h2>
<p>在InnoDB存储引擎中，可以将B+树索引分为聚簇索引和辅助索引（非聚簇索引）。无论是何种索引，每个页的大小都为16KB，且不能更改。</p>
<p><strong>聚簇索引是根据主键创建的一棵B+树，聚簇索引的叶子节点存放了表中的所有记录</strong>。</p>
<p><strong>辅助索引是根据索引键创建的一棵B+树，与聚簇索引不同的是，其叶子节点仅存放索引键值，以及该索引键值指向的主键</strong>。也就是说，如果通过辅助索引来查找数据，那么当找到辅助索引的叶子节点后，很有可能还需要根据主键值查找聚簇索引来得到数据，这种查找方式又被称为书签查找。因为辅助索引不包含行记录的所有数据，这就意味着每页可以存放更多的键值，因此其高度一般都要小于聚簇索引。</p>
<h2 id="1-15-select-in语句中如何使用索引？">1.15 select in语句中如何使用索引？</h2>
<p>索引是否起作用，主要取决于字段类型：</p>
<ul>
<li>如果字段类型为字符串，需要给in查询中的数值与字符串值都需要添加引号，索引才能起作用。</li>
<li>如果字段类型为int，则in查询中的值不需要添加引号，索引也会起作用。</li>
</ul>
<p>IN的字段，在联合索引中，按以上方法，也会起作用。</p>
<h2 id="1-16-模糊查询语句中如何使用索引？">1.16 模糊查询语句中如何使用索引？</h2>
<p>在MySQL中模糊查询 <code>mobile like ‘%8765’</code>，这种情况是不能使用 mobile 上的索引的，那么如果需要根据手机号码后四位进行模糊查询，可以用一下方法进行改造。</p>
<p>我们可以加入冗余列（MySQL5.7之后加入了虚拟列，使用虚拟列更合适，思路相同），比如 mobile_reverse，内部存储为 mobile 的倒叙文本，如 mobile为17312345678，那么 mobile_reverse 存储 87654321371，为 mobile_reverse 列建立索引，查询中使用语句 mobile_reverse like reverse(’%5678’) 即可。</p>
<p>reverse 是 MySQL 中的反转函数，这条语句相当于 mobile_reverse like ‘8765%’ ，这种语句是可以使用索引的。</p>
<h2 id="2-事务">2. 事务</h2>
<h2 id="2-1-事务了解（事务的特性，事务有哪几种类型，它们之间有什么区别？）">2.1 事务了解（事务的特性，事务有哪几种类型，它们之间有什么区别？）</h2>
<p>事务可由一条非常简单的SQL语句组成，也可以由一组复杂的SQL语句组成。在事务中的操作，要么都执行修改，要么都不执行，这就是事务的目的，也是事务模型区别于文件系统的重要特征之一。</p>
<h3 id="事务需遵循ACID四个特性：">事务需遵循ACID四个特性：</h3>
<ul>
<li>**A（atomicity），原子性。**原子性指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功，整个事务的执行才算成功。事务中任何一个SQL语句执行失败，那么已经执行成功的SQL语句也必须撤销，数据库状态应该退回到执行事务前的状态。</li>
<li>**C（consistency），一致性。**一致性指事务将数据库从一种状态转变为另一种一致的状态。在事务开始之前和事务结束以后，数据库的完整性约束没有被破坏。</li>
<li>**I（isolation），隔离性。**事务的隔离性要求每个读写事务的对象与其他事务的操作对象能相互分离，即该事务提交前对其他事务都不可见，这通常使用锁来实现。</li>
<li>**D（durability） ，持久性。**事务一旦提交，其结果就是永久性的，即使发生宕机等故障，数据库也能将数据恢复。持久性保证的是事务系统的高可靠性，而不是高可用性。</li>
</ul>
<p>事务可以分为以下几种类型：</p>
<ul>
<li><strong>扁平事务</strong>：是事务类型中最简单的一种，而在实际生产环境中，这可能是使用最为频繁的事务。在扁平事务中，所有操作都处于同一层次，其由BEGIN WORK开始，由COMMIT WORK或ROLLBACK WORK结束。处于之间的操作是原子的，要么都执行，要么都回滚。</li>
<li><strong>带有保存点的扁平事务</strong>：除了支持扁平事务支持的操作外，允许在事务执行过程中回滚到同一事务中较早的一个状态，这是因为可能某些事务在执行过程中出现的错误并不会对所有的操作都无效，放弃整个事务不合乎要求，开销也太大。保存点（savepoint）用来通知系统应该记住事务当前的状态，以便以后发生错误时，事务能回到该状态。</li>
<li><strong>链事务</strong>：可视为保存点模式的一个变种。链事务的思想是：在提交一个事务时，释放不需要的数据对象，将必要的处理上下文隐式地传给下一个要开始的事务。注意，提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果，就好像在一个事务中进行的。</li>
<li><strong>嵌套事务</strong>：是一个层次结构框架。有一个顶层事务（top-level transaction）控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务（subtransaction），其控制每一个局部的变换。</li>
<li>分布式事务：通常是一个在分布式环境下运行的扁平事务，因此需要根据数据所在位置访问网络中的不同节点。对于分布式事务，同样需要满足ACID特性，要么都发生，要么都失效。</li>
</ul>
<p>对于MySQL的InnoDB存储引擎来说，它支持扁平事务、带有保存点的扁平事务、链事务、分布式事务。对于嵌套事务，MySQL数据库并不是原生的，因此对于有并行事务需求的用户来说MySQL就无能为力了，但是用户可以通过带有保存点的事务来模拟串行的嵌套事务。</p>
<h2 id="2-2-MySQL的ACID特性分别是怎么实现的？">2.2 MySQL的ACID特性分别是怎么实现的？</h2>
<h3 id="原子性实现原理：">原子性实现原理：</h3>
<p>**实现原子性的关键，是当事务回滚时能够撤销所有已经成功执行的sql语句。**<strong>InnoDB实现回滚靠的是undo log</strong>，当事务对数据库进行修改时，InnoDB会生成对应的undo log。如果事务执行失败或调用了rollback，导致事务需要回滚，便可以利用undo log中的信息将数据回滚到修改之前的样子。</p>
<p>undo log属于逻辑日志，它记录的是sql执行相关的信息。当发生回滚时，InnoDB会根据undo log的内容做与之前相反的工作。对于insert，回滚时会执行delete。对于delete，回滚时会执行insert。对于update，回滚时则会执行相反的update，把数据改回去。</p>
<h3 id="持久性实现原理：">持久性实现原理：</h3>
<p>InnoDB作为MySQL的存储引擎，数据是存放在磁盘中的，但如果每次读写数据都需要磁盘IO，效率会很低。为此，<strong>InnoDB提供了缓存(Buffer Pool)</strong>，Buffer Pool中包含了磁盘中部分数据页的映射，作为访问数据库的缓冲。当从数据库读取数据时，会首先从Buffer Pool中读取，如果Buffer Pool中没有，则从磁盘读取后放入Buffer Pool。当向数据库写入数据时，会首先写入Buffer Pool，Buffer Pool中修改的数据会定期刷新到磁盘中（这一过程称为刷脏）。</p>
<p>Buffer Pool的使用大大提高了读写数据的效率，但是也带了新的问题：如果MySQL宕机，而此时Buffer Pool中修改的数据还没有刷新到磁盘，就会导致数据的丢失，事务的持久性无法保证。</p>
<p>于是，<strong>redo log</strong>被引入来解决这个问题。当数据修改时，除了修改Buffer Pool中的数据，还会在redo log记录这次操作。当事务提交时，会调用fsync接口对redo log进行刷盘。如果MySQL宕机，重启时可以读取redo log中的数据，对数据库进行恢复。redo log采用的是WAL（Write-ahead logging，预写式日志），所有修改先写入日志，再更新到Buffer Pool，保证了数据不会因MySQL宕机而丢失，从而满足了持久性要求。</p>
<p>既然redo log也需要在事务提交时将日志写入磁盘，为什么它比直接**将Buffer Pool中修改的数据写入磁盘(即刷脏)**要快呢？主要有以下两方面的原因：</p>
<ul>
<li>刷脏是随机IO，因为每次修改的数据位置随机，但写redo log是追加操作，属于顺序IO。</li>
<li>刷脏是以数据页（Page）为单位的，MySQL默认页大小是16KB，一个Page上一个小修改都要整页写入。而redo log中只包含真正需要写入的部分，无效IO大大减少。</li>
</ul>
<h3 id="隔离性实现原理：">隔离性实现原理：</h3>
<p>**隔离性追求的是并发情形下事务之间互不干扰。**简单起见，我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明)，那么隔离性的探讨，主要可以分为两个方面。</p>
<p><strong>第一方面，(一个事务)写操作对(另一个事务)写操作的影响：锁机制保证隔离性。</strong></p>
<p>隔离性要求同一时刻只能有一个事务对数据进行写操作，InnoDB通过锁机制来保证这一点。锁机制的基本原理可以概括为：事务在修改数据之前，需要先获得相应的锁。获得锁之后，事务便可以修改数据。该事务操作期间，这部分数据是锁定的，其他事务如果需要修改数据，需要等待当前事务提交或回滚后释放锁。</p>
<p>按照粒度，锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表，并发性能较差。行锁则只锁定需要操作的数据，并发性能好。但是由于加锁本身需要消耗资源，因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同的存储引擎支持的锁是不一样的，例如MyIsam只支持表锁，而InnoDB同时支持表锁和行锁，且出于性能考虑，绝大多数情况下使用的都是行锁。</p>
<p><strong>第二方面，(一个事务)写操作对(另一个事务)读操作的影响：MVCC保证隔离性。</strong></p>
<p><strong>InnoDB默认的隔离级别是RR（REPEATABLE READ），RR解决脏读、不可重复读、幻读等问题</strong>，使用的是MVCC。**MVCC全称Multi-Version Concurrency Control，即多版本的并发控制协议。它最大的优点是读不加锁，因此读写不冲突，并发性能好。**InnoDB实现MVCC，多个版本的数据可以共存，主要基于以下技术及数据结构：</p>
<ol>
<li>隐藏列：InnoDB中每行数据都有隐藏列，隐藏列中包含了本行数据的事务id、指向undo log的指针等。</li>
<li>基于undo log的版本链：每行数据的隐藏列中包含了指向undo log的指针，而每条undo log也会指向更早版本的undo log，从而形成一条版本链。</li>
<li>ReadView：通过隐藏列和版本链，MySQL可以将数据恢复到指定版本。但是具体要恢复到哪个版本，则需要根据ReadView来确定。所谓ReadView，是指事务（记做事务A）在某一时刻给整个事务系统（trx_sys）打快照，之后再进行读操作时，会将读取到的数据中的事务id与trx_sys快照比较，从而判断数据对该ReadView是否可见，即对事务A是否可见。</li>
</ol>
<h3 id="一致性实现原理：">一致性实现原理：</h3>
<p>可以说，一致性是事务追求的最终目标。前面提到的原子性、持久性和隔离性，都是为了保证数据库状态的一致性。此外，除了数据库层面的保障，一致性的实现也需要应用层面进行保障。实现一致性的措施包括：</p>
<ul>
<li><strong>保证原子性、持久性和隔离性</strong>，如果这些特性无法保证，事务的一致性也无法保证。</li>
<li><strong>数据库本身提供保障</strong>，例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等。</li>
<li><strong>应用层面进行保障</strong>，例如如果转账操作只扣除转账者的余额，而没有增加接收者的余额，无论数据库实现的多么完美，也无法保证状态的一致。</li>
</ul>
<h2 id="2-3-谈谈MySQL的事务隔离级别">2.3 谈谈MySQL的事务隔离级别</h2>
<p>SQL 标准定义了四种隔离级别，这四种隔离级别分别是：</p>
<ul>
<li>读未提交（READ UNCOMMITTED）；</li>
<li>读提交 （READ COMMITTED）；</li>
<li>可重复读 （REPEATABLE READ）；</li>
<li>串行化 （SERIALIZABLE）。</li>
</ul>
<p>事务隔离是为了解决脏读、不可重复读、幻读问题，下表展示了 4 种隔离级别对这三个问题的解决程度：</p>
<table>
<thead>
<tr>
<th style="text-align:center">隔离级别</th>
<th style="text-align:center">脏读</th>
<th style="text-align:center">不可重复读</th>
<th style="text-align:center">幻读</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">READ UNCOMMITTED</td>
<td style="text-align:center">可能</td>
<td style="text-align:center">可能</td>
<td style="text-align:center">可能</td>
</tr>
<tr>
<td style="text-align:center">READ COMMITTED</td>
<td style="text-align:center">不可能</td>
<td style="text-align:center">可能</td>
<td style="text-align:center">可能</td>
</tr>
<tr>
<td style="text-align:center">REPEATABLE READ</td>
<td style="text-align:center">不可能</td>
<td style="text-align:center">不可能</td>
<td style="text-align:center">可能</td>
</tr>
<tr>
<td style="text-align:center">SERIALIZABLE</td>
<td style="text-align:center">不可能</td>
<td style="text-align:center">不可能</td>
<td style="text-align:center">不可能</td>
</tr>
</tbody>
</table>
<p>上述4种隔离级别MySQL都支持，并且InnoDB存储引擎默认的支持隔离级别是REPEATABLE READ，但是与标准SQL不同的是，<strong>InnoDB存储引擎在REPEATABLE READ事务隔离级别下，使用Next-Key Lock的锁算法，因此避免了幻读的产生</strong>。所以，InnoDB存储引擎在默认的事务隔离级别下已经能完全保证事务的隔离性要求，即达到SQL标准的SERIALIZABLE隔离级别。</p>
<h3 id="脏读、幻读、不可重复读概念：">脏读、幻读、不可重复读概念：</h3>
<ol>
<li>脏读：当前事务(A)中可以读到其他事务(B)未提交的数据（脏数据），这种现象是脏读。</li>
<li>不可重复读：在事务A中先后两次读取同一个数据，两次读取的结果不一样，这种现象称为不可重复读。脏读与不可重复读的区别在于：前者读到的是其他事务未提交的数据，后者读到的是其他事务已提交的数据。</li>
<li>幻读：在事务A中按照某个条件先后两次查询数据库，两次查询结果的条数不同，这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为：前者是数据变了，后者是数据的行数变了。</li>
</ol>
<h2 id="2-4-MySQL的事务隔离级别是怎么实现的？">2.4 MySQL的事务隔离级别是怎么实现的？</h2>
<h3 id="READ-UNCOMMITTED：">READ UNCOMMITTED：</h3>
<p>它是性能最好、也最野蛮的方式，因为它压根儿就不加锁，所以根本谈不上什么隔离效果，可以理解为没有隔离。</p>
<h3 id="SERIALIZABLE：">SERIALIZABLE：</h3>
<p>读的时候加共享锁，其他事务可以并发读，但是不能写。写的时候加排它锁，其他事务不能并发写也不能并发读。</p>
<h3 id="REPEATABLE-READ-READ-COMMITTED：">REPEATABLE READ &amp; READ COMMITTED：</h3>
<h4 id="为了解决不可重复读，MySQL-采用了-MVVC-多版本并发控制-的方式。"><strong>为了解决不可重复读，MySQL 采用了 MVVC (多版本并发控制) 的方式。</strong></h4>
<p>我们在数据库表中看到的一行记录可能实际上有多个版本，每个版本的记录除了有数据本身外，还要有一个表示版本的字段，记为 row trx_id，而这个字段就是使其产生的事务的 id，事务 ID 记为 transaction id，它在事务开始的时候向事务系统申请，按时间先后顺序递增。</p>
<p>如下图，一行记录现在有 3 个版本，每一个版本都记录这使其产生的事务 ID，比如事务A的transaction id 是100，那么版本1的row trx_id 就是 100，同理版本2和版本3。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/tx-1.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>可重复读是在事务开始的时候生成一个当前事务全局性的快照，而读提交则是每次执行语句的时候都重新生成一次快照。对于一个快照来说，它能够读到那些版本数据，要遵循以下规则：</p>
<ol>
<li>当前事务内的更新，可以读到；</li>
<li>版本未提交，不能读到；</li>
<li>版本已提交，但是却在快照创建后提交的，不能读到；</li>
<li>版本已提交，且是在快照创建前提交的，可以读到。</li>
</ol>
<p>再强调一次，两者主要的区别就是在快照的创建上，可重复读仅在事务开始是创建一次，而读提交每次执行语句的时候都要重新创建一次。</p>
<h4 id="MySQL-已经在可重复读隔离级别下解决了幻读的问题，用的是间隙锁。MySQL-把行锁和间隙锁合并在一起，解决了并发写和幻读的问题，这个锁叫做-Next-Key锁。"><strong>MySQL 已经在可重复读隔离级别下解决了幻读的问题，用的是间隙锁。MySQL 把行锁和间隙锁合并在一起，解决了并发写和幻读的问题，这个锁叫做 Next-Key锁。</strong></h4>
<p>假设现在表中有两条记录，并且 age 字段已经添加了索引，两条记录 age 的值分别为 10 和 30。此时，在数据库中会为索引维护一套B+树，用来快速定位行记录。B+索引树是有序的，所以会把这张表的索引分割成几个区间。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/tx-2.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>此时，在数据库中会为索引维护一套B+树，用来快速定位行记录。B+索引树是有序的，所以会把这张表的索引分割成几个区间。如图所示，分成了3 个区间，在这3个区间是可以加间隙锁的。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/tx-3.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>之后，我用下面的两个事务演示一下加锁过程。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/tx-4.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>在事务A提交之前，事务B的插入操作只能等待，这就是间隙锁起得作用。当事务A执行<code>update user set name='风筝2号’ where age = 10;</code> 的时候，由于条件 where age = 10 ，数据库不仅在 age =10 的行上添加了行锁，而且在这条记录的两边，也就是(负无穷,10]、(10,30]这两个区间加了间隙锁，从而导致事务B插入操作无法完成，只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交，age&lt;10、10&lt;age&lt;30 的记录页无法完成，而大于等于30的记录则不受影响，这足以解决幻读问题了。</p>
<p>这是有索引的情况，如果 age 不是索引列，那么数据库会为整个表加上间隙锁。所以，如果是没有索引的话，不管 age 是否大于等于30，都要等待事务A提交才可以成功插入。</p>
<h2 id="2-5-事务可以嵌套吗？">2.5 事务可以嵌套吗？</h2>
<p>可以，因为嵌套事务也是众多事务分类中的一种，它是一个层次结构框架。有一个顶层事务控制着各个层次的事务，顶层事务之下嵌套的事务被称为子事务，它控制每一个局部的变换。</p>
<p>需要注意的是，<strong>MySQL数据库不支持嵌套事务。</strong></p>
<h2 id="2-6-如何实现可重复读？">2.6 如何实现可重复读？</h2>
<p>为了实现可重复读，MySQL 采用了 MVVC (多版本并发控制) 的方式。</p>
<p>我们在数据库表中看到的一行记录可能实际上有多个版本，每个版本的记录除了有数据本身外，还要有一个表示版本的字段，记为 row trx_id，而这个字段就是使其产生的事务的 id，事务 ID 记为 transaction id，它在事务开始的时候向事务系统申请，按时间先后顺序递增。</p>
<p>如下图，一行记录现在有 3 个版本，每一个版本都记录这使其产生的事务 ID，比如事务A的transaction id 是100，那么版本1的row trx_id 就是 100，同理版本2和版本3。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/tx-1-16359116219385.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>可重复读是在事务开始的时候生成一个当前事务全局性的快照。对于一个快照来说，它能够读到那些版本数据，要遵循以下规则：</p>
<ol>
<li>当前事务内的更新，可以读到；</li>
<li>版本未提交，不能读到；</li>
<li>版本已提交，但是却在快照创建后提交的，不能读到；</li>
<li>版本已提交，且是在快照创建前提交的，可以读到。</li>
</ol>
<h2 id="2-7-如何解决幻读问题？">2.7 如何解决幻读问题？</h2>
<p>MySQL 已经在可重复读隔离级别下解决了幻读的问题，用的是间隙锁。MySQL 把行锁和间隙锁合并在一起，解决了并发写和幻读的问题，这个锁叫做 Next-Key锁。</p>
<p>假设现在表中有两条记录，并且 age 字段已经添加了索引，两条记录 age 的值分别为 10 和 30。此时，在数据库中会为索引维护一套B+树，用来快速定位行记录。B+索引树是有序的，所以会把这张表的索引分割成几个区间。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/tx-2-16359116377517.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>此时，在数据库中会为索引维护一套B+树，用来快速定位行记录。B+索引树是有序的，所以会把这张表的索引分割成几个区间。如图所示，分成了3 个区间，在这3个区间是可以加间隙锁的。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/tx-3-16359116377529.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>之后，我用下面的两个事务演示一下加锁过程。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/tx-4-163591163775211.jpg" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>在事务A提交之前，事务B的插入操作只能等待，这就是间隙锁起得作用。当事务A执行<code>update user set name='风筝2号’ where age = 10;</code> 的时候，由于条件 where age = 10 ，数据库不仅在 age =10 的行上添加了行锁，而且在这条记录的两边，也就是(负无穷,10]、(10,30]这两个区间加了间隙锁，从而导致事务B插入操作无法完成，只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交，age&lt;10、10&lt;age&lt;30 的记录页无法完成，而大于等于30的记录则不受影响，这足以解决幻读问题了。</p>
<p>这是有索引的情况，如果 age 不是索引列，那么数据库会为整个表加上间隙锁。所以，如果是没有索引的话，不管 age 是否大于等于30，都要等待事务A提交才可以成功插入。</p>
<h2 id="2-8-MySQL事务如何回滚？">2.8 MySQL事务如何回滚？</h2>
<p>在MySQL默认的配置下，事务都是自动提交和回滚的。当显示地开启一个事务时，可以使用ROLLBACK语句进行回滚。该语句有两种用法：</p>
<ul>
<li>ROLLBACK：要使用这个语句的最简形式，只需发出ROLLBACK。同样地，也可以写为ROLLBACK WORK，但是二者几乎是等价的。回滚会结束用户的事务，并撤销正在进行的所有未提交的修改。</li>
<li>ROLLBACK TO [SAVEPOINT] identifier ：这个语句与SAVEPOINT命令一起使用。可以把事务回滚到标记点，而不回滚在此标记点之前的任何工作。</li>
</ul>
<h2 id="3-锁">3. 锁</h2>
<h2 id="3-1-了解数据库的锁吗">3.1 了解数据库的锁吗</h2>
<p>锁是数据库系统区别于文件系统的一个关键特性，锁机制用于管理对共享资源的并发访问。下面我们以MySQL数据库的InnoDB引擎为例，来说明锁的一些特点。</p>
<h3 id="锁的类型：">锁的类型：</h3>
<p>InnoDB存储引擎实现了如下两种标准的行级锁：</p>
<ul>
<li><strong>共享锁（S Lock），允许事务读一行数据。</strong></li>
<li><strong>排他锁（X Lock），允许事务删除或更新一行数据。</strong></li>
</ul>
<p>如果一个事务T1已经获得了行r的共享锁，那么另外的事务T2可以立即获得行r的共享锁，因为读取并没有改变行r的数据，称这种情况为锁兼容。但若有其他的事务T3想获得行r的排他锁，则其必须等待事务T1、T2释放行r上的共享锁，这种情况称为锁不兼容。下图显示了共享锁和排他锁的兼容性，可以发现X锁与任何的锁都不兼容，而S锁仅和S锁兼容。需要特别注意的是，S和X锁都是行锁，兼容是指对同一记录（row）锁的兼容性情况。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dblock-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h3 id="锁的粒度：">锁的粒度：</h3>
<p>InnoDB存储引擎支持多粒度锁定，这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作，InnoDB存储引擎支持一种额外的锁方式，称之为意向锁。意向锁是将锁定的对象分为多个层次，意向锁意味着事务希望在更细粒度上进行加锁。</p>
<p>InnoDB存储引擎支持意向锁设计比较简练，其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁：</p>
<ul>
<li>意向共享锁（IS Lock），事务想要获得一张表中某几行的共享锁。</li>
<li>意向排他锁（IX Lock），事务想要获得一张表中某几行的排他锁。</li>
</ul>
<p>由于InnoDB存储引擎支持的是行级别的锁，因此意向锁其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如下图所示。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dblock-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h3 id="锁的算法：">锁的算法：</h3>
<p>InnoDB存储引擎有3种行锁的算法，其分别是：</p>
<ul>
<li>Record Lock：单个行记录上的锁。</li>
<li>Gap Lock：间隙锁，锁定一个范围，但不包含记录本身。</li>
<li>Next-Key Lock∶Gap Lock+Record Lock，锁定一个范围，并且锁定记录本身。</li>
</ul>
<p>Record Lock总是会去锁住索引记录，如果InnoDB存储引擎表在建立的时候没有设置任何一个索引，那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法，在Next-Key Lock算法下，InnoDB对于行的查询都是采用这种锁定算法。采用Next-Key Lock的锁定技术称为Next-Key Locking，其设计的目的是为了解决Phantom Problem（幻读）。而利用这种锁定技术，锁定的不是单个值，而是一个范围，是谓词锁（predict lock）的一种改进。</p>
<h3 id="关于死锁：">关于死锁：</h3>
<p><strong>死锁是指两个或两个以上的事务在执行过程中，因争夺锁资源而造成的一种互相等待的现象。若无外力作用，事务都将无法推进下去。</strong></p>
<p>解决死锁问题最简单的一种方法是超时，即当两个事务互相等待时，当一个等待时间超过设置的某一阈值时，其中一个事务进行回滚，另一个等待的事务就能继续进行。</p>
<p>除了超时机制，当前数据库还都普遍采用wait-for graph（等待图）的方式来进行死锁检测。较之超时的解决方案，这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息：</p>
<ul>
<li>锁的信息链表；</li>
<li>事务等待链表；</li>
</ul>
<p>通过上述链表可以构造出一张图，而在这个图中若存在回路，就代表存在死锁，因此资源间相互发生等待。这是一种较为主动的死锁检测机制，在每个事务请求锁并发生等待时都会判断是否存在回路，若存在则有死锁，通常来说InnoDB存储引擎选择回滚undo量最小的事务。</p>
<h3 id="锁的升级：">锁的升级：</h3>
<p>锁升级（Lock Escalation）是指将当前锁的粒度降低。举例来说，数据库可以把一个表的1000个行锁升级为一个页锁，或者将页锁升级为表锁。</p>
<p>InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的，相反，其根据每个事务访问的每个页对锁进行管理的，采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录，其开销通常都是一致的。</p>
<h2 id="3-2-介绍一下间隙锁">3.2 介绍一下间隙锁</h2>
<p>InnoDB存储引擎有3种行锁的算法，间隙锁（Gap Lock）是其中之一。间隙锁用于锁定一个范围，但不包含记录本身。它的作用是为了阻止多个事务将记录插入到同一范围内，而这会导致幻读问题的产生。</p>
<h2 id="3-3-InnoDB中行级锁是怎么实现的？">3.3 InnoDB中行级锁是怎么实现的？</h2>
<p>**InnoDB行级锁是通过给索引上的索引项加锁来实现的。**只有通过索引条件检索数据，InnoDB才使用行级锁，否则，InnoDB将使用表锁。</p>
<p>当表中锁定其中的某几行时，不同的事务可以使用不同的索引锁定不同的行。另外，不论使用主键索引、唯一索引还是普通索引，InnoDB都会使用行锁来对数据加锁。</p>
<h2 id="3-4-数据库在什么情况下会发生死锁？">3.4 数据库在什么情况下会发生死锁？</h2>
<p>死锁是指两个或两个以上的事务在执行过程中，因争夺锁资源而造成的一种互相等待的现象。若无外力作用，事务都将无法推进下去。下图演示了死锁的一种经典的情况，即A等待B、B等待A，这种死锁问题被称为AB-BA死锁。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dblock-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h2 id="3-5-说说数据库死锁的解决办法">3.5 说说数据库死锁的解决办法</h2>
<p><strong>解决死锁问题最简单的一种方法是超时，即当两个事务互相等待时，当一个等待时间超过设置的某一阈值时，其中一个事务进行回滚，另一个等待的事务就能继续进行。</strong></p>
<p>除了超时机制，**当前数据库还都普遍采用wait-for graph（等待图）的方式来进行死锁检测。**较之超时的解决方案，这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息：</p>
<ul>
<li>锁的信息链表；</li>
<li>事务等待链表；</li>
</ul>
<p>通过上述链表可以构造出一张图，而在这个图中若存在回路，就代表存在死锁，因此资源间相互发生等待。这是一种较为主动的死锁检测机制，在每个事务请求锁并发生等待时都会判断是否存在回路，若存在则有死锁，通常来说InnoDB存储引擎选择回滚undo量最小的事务。</p>
<h2 id="4-数据库优化">4. 数据库优化</h2>
<h2 id="4-1-说一说你对数据库优化的理解">4.1 说一说你对数据库优化的理解</h2>
<p>MySQL数据库优化是多方面的，原则是<strong>减少系统的瓶颈，减少资源的占用，增加系统的反应速度</strong>。</p>
<p>例如，<strong>通过优化文件系统，提高磁盘I\O的读写速度；通过优化操作系统调度策略，提高MySQL在高负荷情况下的负载能力</strong>；<strong>优化表结构、索引、查询语句等使查询响应更快</strong>。</p>
<p>针对查询，我们可以通过使用索引、使用连接代替子查询的方式来提高查询速度。</p>
<p>针对慢查询，我们可以通过分析慢查询日志，来发现引起慢查询的原因，从而有针对性的进行优化。</p>
<p>针对插入，我们可以通过禁用索引、禁用检查等方式来提高插入速度，在插入之后再启用索引和检查。</p>
<p>针对数据库结构，我们可以通过将字段很多的表拆分成多张表、增加中间表、增加冗余字段等方式进行优化。</p>
<h2 id="4-2-该如何优化MySQL的查询？（查询优化）">4.2 该如何优化MySQL的查询？（查询优化）</h2>
<h3 id="使用索引：">使用索引：</h3>
<p>如果查询时没有使用索引，查询语句将扫描表中的所有记录。在数据量大的情况下，这样查询的速度会很慢。如果使用索引进行查询，查询语句可以根据索引快速定位到待查询记录，从而减少查询的记录数，达到提高查询速度的目的。</p>
<p>索引可以提高查询的速度，但并不是使用带有索引的字段查询时索引都会起作用。有几种特殊情况，在这些情况下有可能使用带有索引的字段查询时索引并没有起作用。</p>
<ol>
<li>
<p>使用LIKE关键字的查询语句</p>
<p>在使用LIKE关键字进行查询的查询语句中，如果匹配字符串的第一个字符为“%”，索引不会起作用。只有“%”不在第一个位置，索引才会起作用。</p>
</li>
<li>
<p>使用多列索引的查询语句</p>
<p>MySQL可以为多个字段创建索引。一个索引可以包括16个字段。对于多列索引，只有查询条件中使用了这些字段中的第1个字段时索引才会被使用。</p>
</li>
<li>
<p>使用OR关键字的查询语句</p>
<p>查询语句的查询条件中只有OR关键字，且OR前后的两个条件中的列都是索引时，查询中才使用索引。否则，查询将不使用索引。</p>
</li>
</ol>
<h3 id="优化子查询：">优化子查询：</h3>
<p>使用子查询可以进行SELECT语句的嵌套查询，即一个SELECT查询的结果作为另一个SELECT语句的条件。子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作。</p>
<p>子查询虽然可以使查询语句很灵活，但执行效率不高。执行子查询时，MySQL需要为内层查询语句的查询结果建立一个临时表。然后外层查询语句从临时表中查询记录。查询完毕后，再撤销这些临时表。因此，子查询的速度会受到一定的影响。如果查询的数据量比较大，这种影响就会随之增大。</p>
<p>在MySQL中，可以使用连接（JOIN）查询来替代子查询。连接查询不需要建立临时表，其速度比子查询要快，如果查询中使用索引，性能会更好。</p>
<h2 id="4-3-怎样插入数据才能更高效？（插入优化）">4.3 怎样插入数据才能更高效？（插入优化）</h2>
<p>影响插入速度的主要是索引、唯一性校验、一次插入记录条数等。针对这些情况，可以分别进行优化。</p>
<h3 id="对于MyISAM引擎的表，常见的优化方法如下：">对于MyISAM引擎的表，常见的优化方法如下：</h3>
<ol>
<li>
<p>禁用索引</p>
<p>对于非空表，插入记录时，MySQL会根据表的索引对插入的记录建立索引。如果插入大量数据，建立索引会降低插入记录的速度。为了解决这种情况，可以在插入记录之前禁用索引，数据插入完毕后再开启索引。对于空表批量导入数据，则不需要进行此操作，因为MyISAM引擎的表是在导入数据之后才建立索引的。</p>
</li>
<li>
<p>禁用唯一性检查</p>
<p>插入数据时，MySQL会对插入的记录进行唯一性校验。这种唯一性校验也会降低插入记录的速度。为了降低这种情况对查询速度的影响，可以在插入记录之前禁用唯一性检查，等到记录插入完毕后再开启。</p>
</li>
<li>
<p>使用批量插入</p>
<p>插入多条记录时，可以使用一条INSERT语句插入一条记录，也可以使用一条INSERT语句插入多条记录。使用一条INSERT语句插入多条记录的情形如下，而这种方式的插入速度更快。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs mysql">INSERT INTO fruits VALUES<br>(&#x27;x1&#x27;, &#x27;101&#x27;, &#x27;mongo2&#x27;, &#x27;5.7&#x27;),<br>(&#x27;x2&#x27;, &#x27;101&#x27;, &#x27;mongo3&#x27;, &#x27;5.7&#x27;),<br>(&#x27;x3&#x27;, &#x27;101&#x27;, &#x27;mongo4&#x27;, &#x27;5.7&#x27;);<br></code></pre></td></tr></table></figure>
</li>
<li>
<p>使用LOAD DATA INFILE批量导入</p>
<p>当需要批量导入数据时，如果能用LOAD DATA INFILE语句，就尽量使用。因为LOAD DATA INFILE语句导入数据的速度比INSERT语句快。</p>
</li>
</ol>
<h3 id="对于InnoDB引擎的表，常见的优化方法如下：">对于InnoDB引擎的表，常见的优化方法如下：</h3>
<ol>
<li>
<p>禁用唯一性检查</p>
<p>插入数据之前执行<code>set unique_checks=0</code>来禁止对唯一索引的检查，数据导入完成之后再运行<code>set unique_checks=1</code>。这个和MyISAM引擎的使用方法一样。</p>
</li>
<li>
<p>禁用外键检查</p>
<p>插入数据之前执行禁止对外键的检查，数据插入完成之后再恢复对外键的检查。</p>
</li>
<li>
<p>禁用自动提交</p>
<p>插入数据之前禁止事务的自动提交，数据导入完成之后，执行恢复自动提交操作。</p>
</li>
</ol>
<h2 id="4-4-表中包含几千万条数据该怎么办？（数据量-优化）">4.4 表中包含几千万条数据该怎么办？（数据量 优化）</h2>
<p>建议按照如下顺序进行优化：</p>
<ol>
<li>优化SQL和索引；</li>
<li>增加缓存，如memcached、redis；</li>
<li>读写分离，可以采用主从复制，也可以采用主主复制；</li>
<li>使用MySQL自带的分区表，这对应用是透明的，无需改代码，但SQL语句是要针对分区表做优化的；</li>
<li>做垂直拆分，即根据模块的耦合度，将一个大的系统分为多个小的系统；</li>
<li>做水平拆分，要选择一个合理的sharding key，为了有好的查询效率，表结构也要改动，做一定的冗余，应用也要改，sql中尽量带sharding key，将数据定位到限定的表上去查，而不是扫描全部的表。</li>
</ol>
<h2 id="4-5-MySQL的慢查询优化有了解吗？">4.5  MySQL的慢查询优化有了解吗？</h2>
<p>优化MySQL的慢查询，可以按照如下步骤进行：</p>
<h3 id="开启慢查询日志：">开启慢查询日志：</h3>
<p>MySQL中慢查询日志默认是关闭的，可以通过配置文件my.ini或者my.cnf中的log-slow-queries选项打开，也可以在MySQL服务启动的时候使用<code>--log-slow-queries[=file_name]</code>启动慢查询日志。</p>
<p>启动慢查询日志时，需要在my.ini或者my.cnf文件中配置long_query_time选项指定记录阈值，<strong>如果某条查询语句的查询时间超过了这个值，这个查询过程将被记录到慢查询日志文件中。</strong></p>
<h3 id="分析慢查询日志：">分析慢查询日志：</h3>
<p>直接分析mysql慢查询日志，<strong>利用explain关键字可以模拟优化器执行SQL查询语句，来分析sql慢查询语句</strong>。</p>
<h3 id="常见慢查询优化：">常见慢查询优化：</h3>
<ol>
<li>
<h4 id="索引没起作用的情况">索引没起作用的情况</h4>
<ul>
<li>在使用LIKE关键字进行查询的查询语句中，如果匹配字符串的第一个字符为“%”，索引不会起作用。只有“%”不在第一个位置，索引才会起作用。</li>
<li>MySQL可以为多个字段创建索引。一个索引可以包括16个字段。对于多列索引，只有查询条件中使用了这些字段中的第1个字段时索引才会被使用。</li>
<li>查询语句的查询条件中只有OR关键字，且OR前后的两个条件中的列都是索引时，查询中才使用索引。否则，查询将不使用索引。</li>
</ul>
</li>
<li>
<h4 id="优化数据库结构">优化数据库结构</h4>
<ul>
<li>对于字段比较多的表，如果有些字段的使用频率很低，可以将这些字段分离出来形成新表。因为当一个表的数据量很大时，会由于使用频率低的字段的存在而变慢。</li>
<li>对于需要经常联合查询的表，可以建立中间表以提高查询效率。通过建立中间表，把需要经常联合查询的数据插入到中间表中，然后将原来的联合查询改为对中间表的查询，以此来提高查询效率。</li>
</ul>
</li>
<li>
<h4 id="分解关联查询">分解关联查询</h4>
<p>很多高性能的应用都会对关联查询进行分解，就是可以对每一个表进行一次单表查询，然后将查询结果在应用程序中进行关联，很多场景下这样会更高效。</p>
</li>
<li>
<h4 id="优化LIMIT分页">优化LIMIT分页</h4>
<p>当偏移量非常大的时候，例如可能是limit 10000,20这样的查询，这是mysql需要查询10020条然后只返回最后20条，前面的10000条记录都将被舍弃，这样的代价很高。优化此类查询的一个最简单的方法是尽可能的使用索引覆盖扫描，而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候这样做的效率会得到很大提升。</p>
</li>
</ol>
<h2 id="4-6-说一说你对explain的了解">4.6 说一说你对explain的了解</h2>
<p>MySQL中提供了EXPLAIN语句和DESCRIBE语句，用来分析查询语句，EXPLAIN语句的基本语法如下：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">EXPLAIN [EXTENDED] SELECT select_options<br></code></pre></td></tr></table></figure>
<p>使用EXTENED关键字，EXPLAIN语句将产生附加信息。执行该语句，可以分析EXPLAIN后面SELECT语句的执行情况，并且能够分析出所查询表的一些特征。下面对查询结果进行解释：</p>
<ul>
<li>id：SELECT识别符。这是SELECT的查询序列号。</li>
<li>select_type：表示SELECT语句的类型。</li>
<li>table：表示查询的表。</li>
<li>type：表示表的连接类型。</li>
<li>possible_keys：给出了MySQL在搜索数据记录时可选用的各个索引。</li>
<li>key：是MySQL实际选用的索引。</li>
<li>key_len：给出索引按字节计算的长度，key_len数值越小，表示越快。</li>
<li>ref：给出了关联关系中另一个数据表里的数据列名。</li>
<li>rows：是MySQL在执行这个查询时预计会从这个数据表里读出的数据行的个数。</li>
<li>Extra：提供了与关联操作有关的信息。</li>
</ul>
<p><strong>扩展阅读</strong></p>
<p>DESCRIBE语句的使用方法与EXPLAIN语句是一样的，分析结果也是一样的，并且可以缩写成DESC。。DESCRIBE语句的语法形式如下：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mysql">DESCRIBE SELECT select_options<br></code></pre></td></tr></table></figure>
<h2 id="4-7-explain关注什么？">4.7 explain关注什么？</h2>
<p>重点要关注如下几列：</p>
<table>
<thead>
<tr>
<th style="text-align:center">列名</th>
<th style="text-align:center">备注</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">type</td>
<td style="text-align:center">本次查询表联接类型，从这里可以看到本次查询大概的效率。</td>
</tr>
<tr>
<td style="text-align:center">key</td>
<td style="text-align:center">最终选择的索引，如果没有索引的话，本次查询效率通常很差。</td>
</tr>
<tr>
<td style="text-align:center">key_len</td>
<td style="text-align:center">本次查询用于结果过滤的索引实际长度。</td>
</tr>
<tr>
<td style="text-align:center">rows</td>
<td style="text-align:center">预计需要扫描的记录数，预计需要扫描的记录数越小越好。</td>
</tr>
<tr>
<td style="text-align:center">Extra</td>
<td style="text-align:center">额外附加信息，主要确认是否出现 Using filesort、Using temporary 这两种情况。</td>
</tr>
</tbody>
</table>
<p>其中，type包含以下几种结果，从上之下依次是最差到最好：</p>
<table>
<thead>
<tr>
<th style="text-align:center">类型</th>
<th style="text-align:center">备注</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">ALL</td>
<td style="text-align:center">执行full table scan，这是最差的一种方式。</td>
</tr>
<tr>
<td style="text-align:center">index</td>
<td style="text-align:center">执行full index scan，并且可以通过索引完成结果扫描并且直接从索引中取的想要的结果数据，也就是可以避免回表，比ALL略好，因为索引文件通常比全部数据要来的小。</td>
</tr>
<tr>
<td style="text-align:center">range</td>
<td style="text-align:center">利用索引进行范围查询，比index略好。</td>
</tr>
<tr>
<td style="text-align:center">index_subquery</td>
<td style="text-align:center">子查询中可以用到索引。</td>
</tr>
<tr>
<td style="text-align:center">unique_subquery</td>
<td style="text-align:center">子查询中可以用到唯一索引，效率比 index_subquery 更高些。</td>
</tr>
<tr>
<td style="text-align:center">index_merge</td>
<td style="text-align:center">可以利用index merge特性用到多个索引，提高查询效率。</td>
</tr>
<tr>
<td style="text-align:center">ref_or_null</td>
<td style="text-align:center">表连接类型是ref，但进行扫描的索引列中可能包含NULL值。</td>
</tr>
<tr>
<td style="text-align:center">fulltext</td>
<td style="text-align:center">全文检索。</td>
</tr>
<tr>
<td style="text-align:center">ref</td>
<td style="text-align:center">基于索引的等值查询，或者表间等值连接。</td>
</tr>
<tr>
<td style="text-align:center">eq_ref</td>
<td style="text-align:center">表连接时基于主键或非NULL的唯一索引完成扫描，比ref略好。</td>
</tr>
<tr>
<td style="text-align:center">const</td>
<td style="text-align:center">基于主键或唯一索引唯一值查询，最多返回一条结果，比eq_ref略好。</td>
</tr>
<tr>
<td style="text-align:center">system</td>
<td style="text-align:center">查询对象表只有一行数据，这是最好的情况。</td>
</tr>
</tbody>
</table>
<p>另外，Extra列需要注意以下的几种情况：</p>
<table>
<thead>
<tr>
<th style="text-align:center">关键字</th>
<th style="text-align:center">备注</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">Using filesort</td>
<td style="text-align:center">将用外部排序而不是按照索引顺序排列结果，数据较少时从内存排序，否则需要在磁盘完成排序，代价非常高，需要添加合适的索引。</td>
</tr>
<tr>
<td style="text-align:center">Using temporary</td>
<td style="text-align:center">需要创建一个临时表来存储结果，这通常发生在对没有索引的列进行GROUP BY时，或者ORDER BY里的列不都在索引里，需要添加合适的索引。</td>
</tr>
<tr>
<td style="text-align:center">Using index</td>
<td style="text-align:center">表示MySQL使用覆盖索引避免全表扫描，不需要再到表中进行二次查找数据，这是比较好的结果之一。注意不要和type中的index类型混淆。</td>
</tr>
<tr>
<td style="text-align:center">Using where</td>
<td style="text-align:center">通常是进行了全表/全索引扫描后再用WHERE子句完成结果过滤，需要添加合适的索引。</td>
</tr>
<tr>
<td style="text-align:center">Impossible WHERE</td>
<td style="text-align:center">对Where子句判断的结果总是false而不能选择任何数据，例如where 1=0，无需过多关注。</td>
</tr>
<tr>
<td style="text-align:center">Select tables optimized away</td>
<td style="text-align:center">使用某些聚合函数来访问存在索引的某个字段时，优化器会通过索引直接一次定位到所需要的数据行完成整个查询，例如MIN()\MAX()，这种也是比较好的结果之一。</td>
</tr>
</tbody>
</table>
<h2 id="5-其他内容（基础内容等）">5. 其他内容（基础内容等）</h2>
<h2 id="5-1-数据库三大范式">5.1 数据库三大范式</h2>
<p>目前关系数据库有六种范式，一般来说，数据库只需满足第三范式(3NF）就行了。</p>
<h3 id="第一范式（1NF）：">第一范式（1NF）：</h3>
<p>是指在关系模型中，对于添加的一个规范要求，所有的域都应该是原子性的，即<strong>数据库表的每一列都是不可分割的原子数据项，而不能是集合，数组，记录等非原子数据项</strong>。</p>
<p>即实体中的某个属性有多个值时，必须拆分为不同的属性。在符合第一范式表中的每个域值只能是实体的一个属性或一个属性的一部分。简而言之，第一范式就是无重复的域。</p>
<h3 id="第二范式（2NF）：">第二范式（2NF）：</h3>
<p>在1NF的基础上，非码属性必须完全依赖于候选码（在1NF基础上消除非主属性对主码的部分函数依赖）。</p>
<p>第二范式是在第一范式的基础上建立起来的，即满足第二范式必须先满足第一范式。<strong>第二范式要求数据库表中的每个实例或记录必须可以被唯一地区分。选取一个能区分每个实体的属性或属性组，作为实体的唯一标识。</strong></p>
<p>例如在员工表中的身份证号码即可实现每个一员工的区分，该身份证号码即为候选键，任何一个候选键都可以被选作主键。在找不到候选键时，可额外增加属性以实现区分，如果在员工关系中，没有对其身份证号进行存储，而姓名可能会在数据库运行的某个时间重复，无法区分出实体时，设计辟如ID等不重复的编号以实现区分，被添加的编号或ID选作主键。</p>
<h3 id="第三范式（3NF）：">第三范式（3NF）：</h3>
<p>在2NF基础上，任何非主属性不依赖于其它非主属性（在2NF基础上消除传递依赖）。</p>
<p>第三范式是第二范式的一个子集，即满足第三范式必须满足第二范式。简而言之，<strong>第三范式要求一个关系中不包含已在其它关系已包含的非主关键字信息。</strong></p>
<p>例如，存在一个部门信息表，其中每个部门有部门编号（dept_id）、部门名称、部门简介等信息。那么在员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表，则根据第三范式（3NF）也应该构建它，否则就会有大量的数据冗余。</p>
<h2 id="5-2-说一说你对MySQL引擎的了解">5.2 说一说你对MySQL引擎的了解</h2>
<p>MySQL提供了多个不同的存储引擎，<strong>包括处理事务安全表的引擎和处理非事务安全表的引擎</strong>。在MySQL中，不需要在整个服务器中使用同一种存储引擎，针对具体的要求，可以对每一个表使用不同的存储引擎。MySQL 8.0支持的存储引擎有<strong>InnoDB、MyISAM、Memory、Merge、Archive、Federated、CSV、BLACKHOLE</strong>等。其中，<strong>最常用的引擎是InnoDB和MyISAM。</strong></p>
<h3 id="InnoDB存储引擎：">InnoDB存储引擎：</h3>
<p>InnoDB是<strong>事务型数据库</strong>的首选引擎，<strong>支持事务安全表（ACID）</strong>，<strong>支持行锁定和外键</strong>。MySQL 5.5.5之后，InnoDB作为默认存储引擎，主要特性如下：</p>
<ol>
<li>**InnoDB给MySQL提供了具有提交、回滚和崩溃恢复能力的事务安全（ACID兼容）存储引擎。**InnoDB锁定在行级并且也在SELECT语句中提供一个类似Oracle的非锁定读。这些功能增加了多用户部署和性能。在SQL查询中，可以自由地将InnoDB类型的表与其他MySQL表的类型混合起来，甚至在同一个查询中也可以混合。</li>
<li>**InnoDB是为处理巨大数据量的最大性能设计。**它的CPU效率可能是任何其他基于磁盘的关系数据库引擎所不能匹敌的。</li>
<li>**InnoDB存储引擎完全与MySQL服务器整合，为在主内存中缓存数据和索引而维持它自己的缓冲池。**InnoDB将它的表和索引存在一个逻辑表空间中，表空间可以包含数个文件（或原始磁盘分区）。这与MyISAM表不同，比如在MyISAM表中每个表被存在分离的文件中。InnoDB表可以是任何尺寸，即使在文件尺寸被限制为2GB的操作系统上。</li>
<li>**InnoDB支持外键完整性约束（FOREIGN KEY）。**存储表中的数据时，每张表的存储都按主键顺序存放，如果没有显示在表定义时指定主键，InnoDB会为每一行生成一个6B的ROWID，并以此作为主键。</li>
<li>**InnoDB被用在众多需要高性能的大型数据库站点上。**InnoDB不创建目录，使用InnoDB时，MySQL将在数据目录下创建一个名为ibdata1的10MB大小的自动扩展数据文件，以及两个名为ib_logfile0和ib_logfile1的5MB大小的日志文件。</li>
</ol>
<h3 id="MyISAM存储引擎：">MyISAM存储引擎：</h3>
<p>MyISAM基于ISAM存储引擎，并对其进行扩展。它是在Web、数据仓储和其他应用环境下最常使用的存储引擎之一。**MyISAM拥有较高的插入、查询速度，但不支持事务。**MyISAM的主要特性如下：</p>
<ol>
<li>在支持大文件（达63位文件长度）的文件系统和操作系统上被支持。</li>
<li>当把删除和更新及插入操作混合使用的时候，动态尺寸的行产生更少碎片。这要通过合并相邻被删除的块以及若下一个块被删除则扩展到下一块来自动完成。</li>
<li>每个MyISAM表最大的索引数是64，这可以通过重新编译来改变。每个索引最大的列数是16个。</li>
<li>最大的键长度是1000B，这也可以通过编译来改变。对于键长度超过250B的情况，一个超过1024B的键将被用上。</li>
<li>BLOB和TEXT列可以被索引。</li>
<li>NULL值被允许在索引的列中，这个值占每个键的0~1个字节。</li>
<li>所有数字键值以高字节优先被存储，以允许一个更高的索引压缩。</li>
<li>每个表一个AUTO_INCREMENT列的内部处理。MyISAM为INSERT和UPDATE操作自动更新这一列，这使得AUTO_INCREMENT列更快（至少10%）。在序列顶的值被删除之后就不能再利用。</li>
<li>可以把数据文件和索引文件放在不同目录。</li>
<li>每个字符列可以有不同的字符集。</li>
<li>有VARCHAR的表可以固定或动态记录长度。</li>
<li>VARCHAR和CHAR列可以多达64KB。</li>
</ol>
<h2 id="5-3-说一说你对redo-log、undo-log、binlog的了解">5.3 说一说你对redo log、undo log、binlog的了解</h2>
<h3 id="binlog（Binary-Log）：">binlog（Binary Log）：</h3>
<p>二进制日志文件就是常说的binlog。<strong>二进制日志记录了MySQL所有修改数据库的操作，然后以二进制的形式记录在日志文件中，其中还包括每条语句所执行的时间和所消耗的资源，以及相关的事务信息。</strong></p>
<p>默认情况下，二进制日志功能是开启的，启动时可以重新配置<code>--log-bin[=file_name]</code>选项，修改二进制日志存放的目录和文件名称。</p>
<h3 id="redo-log：">redo log：</h3>
<p>**重做日志用来实现事务的持久性，即事务ACID中的D。**它由两部分组成：一是内存中的重做日志缓冲（redo log buffer），其是易失的；二是重做日志文件（redo log file），它是持久的。</p>
<p>InnoDB是事务的存储引擎，它通过Force Log at Commit机制实现事务的持久性，即当事务提交（COMMIT）时，必须先将该事务的所有日志写入到重做日志文件进行持久化，待事务的COMMIT操作完成才算完成。这里的日志是指重做日志，在InnoDB存储引擎中，由两部分组成，即redo log和undo log。</p>
<p>**redo log用来保证事务的持久性，undo log用来帮助事务回滚及MVCC的功能。**redo log基本上都是顺序写的，在数据库运行时不需要对redo log的文件进行读取操作。而undo log是需要进行随机读写的。</p>
<h3 id="undo-log：">undo log：</h3>
<p>**重做日志记录了事务的行为，可以很好地通过其对页进行“重做”操作。<strong>但是</strong>事务有时还需要进行回滚操作，这时就需要undo。**因此在对数据库进行修改时，InnoDB存储引擎不但会产生redo，还会产生一定量的undo。这样如果用户执行的事务或语句由于某种原因失败了，又或者用户用一条ROLLBACK语句请求回滚，就可以利用这些undo信息将数据回滚到修改之前的样子。</p>
<p>redo存放在重做日志文件中，与redo不同，undo存放在数据库内部的一个特殊段（segment）中，这个段称为undo段（undo segment），undo段位于共享表空间内。</p>
<h2 id="5-4-谈谈你对MVCC的了解">5.4 谈谈你对MVCC的了解</h2>
<p>InnoDB默认的隔离级别是RR（REPEATABLE READ），RR解决脏读、不可重复读、幻读等问题，使用的是MVCC。MVCC全称Multi-Version Concurrency Control，即多版本的并发控制协议。**它最大的优点是读不加锁，因此读写不冲突，并发性能好。**InnoDB实现MVCC，多个版本的数据可以共存，主要基于以下技术及数据结构：</p>
<ol>
<li>隐藏列：InnoDB中每行数据都有隐藏列，隐藏列中包含了本行数据的事务id、指向undo log的指针等。</li>
<li>基于undo log的版本链：每行数据的隐藏列中包含了指向undo log的指针，而每条undo log也会指向更早版本的undo log，从而形成一条版本链。</li>
<li>ReadView：通过隐藏列和版本链，MySQL可以将数据恢复到指定版本。但是具体要恢复到哪个版本，则需要根据ReadView来确定。所谓ReadView，是指事务（记做事务A）在某一时刻给整个事务系统（trx_sys）打快照，之后再进行读操作时，会将读取到的数据中的事务id与trx_sys快照比较，从而判断数据对该ReadView是否可见，即对事务A是否可见。</li>
</ol>
<h2 id="5-5-MySQL主从同步是如何实现的？">5.5 MySQL主从同步是如何实现的？</h2>
<p>复制（replication）是MySQL数据库提供的一种高可用高性能的解决方案，一般用来建立大型的应用。总体来说，replication的工作原理分为以下3个步骤：</p>
<ol>
<li>主服务器（master）把数据更改记录到二进制日志（binlog）中。</li>
<li>从服务器（slave）把主服务器的二进制日志复制到自己的中继日志（relay log）中。</li>
<li>从服务器重做中继日志中的日志，把更改应用到自己的数据库上，以达到数据的最终一致性。</li>
</ol>
<p>复制的工作原理并不复杂，其实就是一个完全备份加上二进制日志备份的还原。不同的是这个二进制日志的还原操作基本上实时在进行中。这里特别需要注意的是，复制不是完全实时地进行同步，而是异步实时。这中间存在主从服务器之间的执行延时，如果主服务器的压力很大，则可能导致主从服务器延时较大。复制的工作原理如下图所示，其中从服务器有2个线程，一个是I/O线程，负责读取主服务器的二进制日志，并将其保存为中继日志；另一个是SQL线程，复制执行中继日志。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dbreplication.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h1>4. Redis</h1>
<h2 id="1-1-Redis可以用来做什么？">1.1 Redis可以用来做什么？</h2>
<p><strong>参考答案</strong></p>
<ol>
<li>Redis最常用来做缓存，是实现分布式缓存的首先中间件；</li>
<li>Redis可以作为数据库，实现诸如点赞、关注、排行等对性能要求极高的互联网需求；</li>
<li>Redis可以作为计算工具，能用很小的代价，统计诸如PV/UV、用户在线天数等数据；</li>
<li>Redis还有很多其他的使用场景，例如：可以实现分布式锁，可以作为消息队列使用。</li>
</ol>
<h2 id="1-2-Redis和传统的关系型数据库有什么不同？">1.2 Redis和传统的关系型数据库有什么不同？</h2>
<p><strong>参考答案</strong></p>
<p>Redis是一种基于键值对的NoSQL数据库，而键值对的值是由多种数据结构和算法组成的。Redis的数据都存储于内存中，因此它的速度惊人，读写性能可达10万/秒，远超关系型数据库。</p>
<p>关系型数据库是基于二维数据表来存储数据的，它的数据格式更为严谨，并支持关系查询。关系型数据库的数据存储于磁盘上，可以存放海量的数据，但性能远不如Redis。</p>
<h2 id="1-3-Redis有哪些数据类型？">1.3 Redis有哪些数据类型？</h2>
<p><strong>参考答案</strong></p>
<ol>
<li>Redis支持5种核心的数据类型，分别是字符串、哈希、列表、集合、有序集合；</li>
<li>Redis还提供了Bitmap、HyperLogLog、Geo类型，但这些类型都是基于上述核心数据类型实现的；</li>
<li>Redis在5.0新增加了Streams数据类型，它是一个功能强大的、支持多播的、可持久化的消息队列。</li>
</ol>
<h2 id="1-4-Redis是单线程的，为什么还能这么快？">1.4 Redis是单线程的，为什么还能这么快？</h2>
<p><strong>参考答案</strong></p>
<ol>
<li>对服务端程序来说，线程切换和锁通常是性能杀手，而单线程避免了线程切换和竞争所产生的消耗；</li>
<li>Redis的大部分操作是在内存上完成的，这是它实现高性能的一个重要原因；</li>
<li>Redis采用了IO多路复用机制，使其在网络IO操作中能并发处理大量的客户端请求，实现高吞吐率。</li>
</ol>
<p>关于Redis的单线程架构实现，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h2 id="1-5-Redis在持久化时fork出一个子进程，这时已经有两个进程了，怎么能说是单线程呢？">1.5 Redis在持久化时fork出一个子进程，这时已经有两个进程了，怎么能说是单线程呢？</h2>
<p><strong>参考答案</strong></p>
<p>Redis是单线程的，**主要是指Redis的网络IO和键值对读写是由一个线程来完成的。<strong>而Redis的其他功能，如</strong>持久化、异步删除、集群数据同步等，则是依赖其他线程来执行的。**所以，说Redis是单线程的只是一种习惯的说法，事实上它的底层不是单线程的。</p>
<h2 id="1-6-set和zset有什么区别？">1.6 set和zset有什么区别？</h2>
<p><strong>参考答案</strong></p>
<p>set：</p>
<ul>
<li>集合中的元素是无序、不可重复的，一个集合最多能存储2^32 - 1个元素；</li>
<li>集合除了支持对元素的增删改查之外，还支持对多个集合取交集、并集、差集。</li>
</ul>
<p>zset：</p>
<ul>
<li>有序集合保留了集合元素不能重复的特点；</li>
<li>有序集合会给每个元素设置一个分数，并以此作为排序的依据；</li>
<li>有序集合不能包含相同的元素，但是不同元素的分数可以相同。</li>
</ul>
<h2 id="1-7-说一下Redis中的watch命令">1.7 说一下Redis中的watch命令</h2>
<p><strong>参考答案</strong></p>
<p>很多时候，要确保事务中的数据没有被其他客户端修改才执行该事务。Redis提供了watch命令来解决这类问题，这是一种乐观锁的机制。客户端通过watch命令，要求服务器对一个或多个key进行监视，如果在客户端执行事务之前，这些key发生了变化，则服务器将拒绝执行客户端提交的事务，并向它返回一个空值。</p>
<h2 id="1-8-说说Redis中List结构的相关操作">1.8 说说Redis中List结构的相关操作</h2>
<p><strong>参考答案</strong></p>
<p>列表是线性有序的数据结构，它内部的元素是可以重复的，并且一个列表最多能存储2^32-1个元素。列表包含如下的常用命令：</p>
<ul>
<li>lpush/rpush：从列表的左侧/右侧添加数据；</li>
<li>lrange：指定索引范围，并返回这个范围内的数据；</li>
<li>lindex：返回指定索引处的数据；</li>
<li>lpop/rpop：从列表的左侧/右侧弹出一个数据；</li>
<li>blpop/brpop：从列表的左侧/右侧弹出一个数据，若列表为空则进入阻塞状态。</li>
</ul>
<h2 id="1-9-你要如何设计Redis的过期时间？">1.9 你要如何设计Redis的过期时间？</h2>
<p><strong>参考答案</strong></p>
<ol>
<li>热点数据不设置过期时间，使其达到“物理”上的永不过期，可以避免缓存击穿问题；</li>
<li>在设置过期时间时，可以附加一个随机数，避免大量的key同时过期，导致缓存雪崩。</li>
</ol>
<h2 id="1-10-Redis中，sexnx命令的返回值是什么，如何使用该命令实现分布式锁？">1.10 Redis中，sexnx命令的返回值是什么，如何使用该命令实现分布式锁？</h2>
<p><strong>参考答案</strong></p>
<p>setnx命令返回整数值，当返回1时表示设置值成果，当返回0时表示设置值失败（key已存在）。</p>
<p>一般我们不建议直接使用setnx命令来实现分布式锁，因为为了避免出现死锁，我们要给锁设置一个自动过期时间。而setnx命令和设置过期时间的命令不是原子的，可能加锁成果而设置过期时间失败，依然存在死锁的隐患。对于这种情况，Redis改进了set命令，给它增加了nx选项，启用该选项时set命令的效果就会setnx一样了。</p>
<p>采用Redis实现分布式锁，就是在Redis里存一份代表锁的数据，通常用字符串即可。采用改进后的setnx命令（即<code>set...nx...</code>命令）实现分布式锁的思路，以及优化的过程如下：</p>
<p>加锁：</p>
<p>第一版，这种方式的缺点是容易产生死锁，因为客户端有可能忘记解锁，或者解锁失败。</p>
<figure class="highlight ebnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs ebnf"><span class="hljs-attribute">setnx key value</span><br></code></pre></td></tr></table></figure>
<p>第二版，给锁增加了过期时间，避免出现死锁。但这两个命令不是原子的，第二步可能会失败，依然无法避免死锁问题。</p>
<figure class="highlight gauss"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs gauss">setnx <span class="hljs-built_in">key</span> value``expire <span class="hljs-built_in">key</span> seconds<br></code></pre></td></tr></table></figure>
<p>第三版，通过“set…nx…”命令，将加锁、过期命令编排到一起，它们是原子操作了，可以避免死锁。</p>
<figure class="highlight gams"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs gams"><span class="hljs-keyword">set</span> key <span class="hljs-comment">value nx ex seconds</span><br></code></pre></td></tr></table></figure>
<p>解锁：</p>
<p>解锁就是删除代表锁的那份数据。</p>
<figure class="highlight maxima"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs maxima"><span class="hljs-built_in">del</span> <span class="hljs-built_in">key</span><br></code></pre></td></tr></table></figure>
<p>问题：</p>
<p>看起来已经很完美了，但实际上还有隐患，如下图。进程A在任务没有执行完毕时，锁已经到期被释放了。等进程A的任务执行结束后，它依然会尝试释放锁，因为它的代码逻辑就是任务结束后释放锁。但是，它的锁早已自动释放过了，它此时释放的可能是其他线程的锁。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>想要解决这个问题，我们需要解决两件事情：</p>
<ol>
<li>在加锁时就要给锁设置一个标识，进程要记住这个标识。当进程解锁的时候，要进行判断，是自己持有的锁才能释放，否则不能释放。可以为key赋一个随机值，来充当进程的标识。</li>
<li>解锁时要先判断、再释放，这两步需要保证原子性，否则第二步失败的话，就会出现死锁。而获取和删除命令不是原子的，这就需要采用Lua脚本，通过Lua脚本将两个命令编排在一起，而整个Lua脚本的执行是原子的。</li>
</ol>
<p>按照以上思路，优化后的命令如下：</p>
<p><a target="_blank" rel="noopener" href="https://www.nowcoder.com/tutorial/10070/d1df94f5f1354e20a897c08aa99f8629#">复制代码</a></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs mysql"># 加锁<br>set key random-value nx ex seconds<br><br># 解锁<br>if redis.call(``&quot;get&quot;``,KEYS[``1``]) == ARGV[``1``] then<br>	return` `redis.call(``&quot;del&quot;``,KEYS[``1``])<br>else<br>	return 0<br>end<br></code></pre></td></tr></table></figure>
<h2 id="1-11-说一说Redis的持久化策略">1.11 说一说Redis的持久化策略</h2>
<p><strong>参考答案</strong></p>
<h5 id="Redis支持RDB持久化、AOF持久化、RDB-AOF混合持久化这三种持久化方式。">Redis支持RDB持久化、AOF持久化、RDB-AOF混合持久化这三种持久化方式。</h5>
<h5 id="RDB：">RDB：</h5>
<p>**RDB(Redis Database)是Redis默认采用的持久化方式，它以快照的形式将进程数据持久化到硬盘中。**RDB会创建一个经过压缩的二进制文件，文件以“.rdb”结尾，内部存储了各个数据库的键值对数据等信息。RDB持久化的触发方式有两种：</p>
<ul>
<li>手动触发：通过<strong>SAVE或BGSAVE</strong>命令触发RDB持久化操作，创建“.rdb”文件；</li>
<li>自动触发：通过配置选项，让服务器在满足指定条件时自动执行BGSAVE命令。</li>
</ul>
<p>其中，SAVE命令执行期间，Redis服务器将阻塞，直到“.rdb”文件创建完毕为止。而BGSAVE命令是异步版本的SAVE命令，它会使用Redis服务器进程的子进程，创建“.rdb”文件。BGSAVE命令在创建子进程时会存在短暂的阻塞，之后服务器便可以继续处理其他客户端的请求。总之，BGSAVE命令是针对SAVE阻塞问题做的优化，Redis内部所有涉及RDB的操作都采用BGSAVE的方式，而SAVE命令已经废弃！</p>
<p>BGSAVE命令的执行流程，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>BGSAVE命令的原理，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-4.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h5 id="RDB持久化的优缺点如下：">RDB持久化的优缺点如下：</h5>
<ul>
<li>
<p>优点：RDB生成紧凑压缩的二进制文件，体积小，使用该文件恢复数据的速度非常快；</p>
</li>
<li>
<p>缺点：BGSAVE每次运行都要执行fork操作创建子进程，属于重量级操作，不宜频繁执行，</p>
<p>所以RDB持久化没办法做到实时的持久化。</p>
</li>
</ul>
<h5 id="AOF：">AOF：</h5>
<p>AOF（Append Only File），解决了数据持久化的实时性，是目前Redis持久化的主流方式。AOF以独立日志的方式，记录了每次写入命令，重启时再重新执行AOF文件中的命令来恢复数据。AOF的工作流程包括：命令写入（append）、文件同步（sync）、文件重写（rewrite）、重启加载（load），如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-5.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>AOF默认不开启，需要修改配置项来启用它：</p>
<figure class="highlight autohotkey"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs autohotkey">appendonly yes              # 启用AOF``appendfilename ``<span class="hljs-string">&quot;appendonly.aof&quot;</span>`   `# 设置文件名<br></code></pre></td></tr></table></figure>
<p>AOF以文本协议格式写入命令，如：</p>
<figure class="highlight taggerscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs taggerscript">*``3``<span class="hljs-symbol">\r</span><span class="hljs-symbol">\n</span>$``3``<span class="hljs-symbol">\r</span><span class="hljs-symbol">\n</span>set<span class="hljs-symbol">\r</span><span class="hljs-symbol">\n</span>$``5``<span class="hljs-symbol">\r</span><span class="hljs-symbol">\n</span>hello<span class="hljs-symbol">\r</span><span class="hljs-symbol">\n</span>$``5``<span class="hljs-symbol">\r</span><span class="hljs-symbol">\n</span>world<span class="hljs-symbol">\r</span><span class="hljs-symbol">\n</span><br></code></pre></td></tr></table></figure>
<p>文本协议格式具有如下的优点：</p>
<ol>
<li>文本协议具有很好的兼容性；</li>
<li>直接采用文本协议格式，可以避免二次处理的开销；</li>
<li>文本协议具有可读性，方便直接修改和处理。</li>
</ol>
<h5 id="AOF持久化的文件同步机制：">AOF持久化的文件同步机制：</h5>
<p>为了提高程序的写入性能，现代操作系统会把针对硬盘的多次写操作优化为一次写操作。</p>
<ol>
<li>当程序调用write对文件写入时，系统不会直接把书记写入硬盘，而是先将数据写入内存的缓冲区中；</li>
<li>当达到特定的时间周期或缓冲区写满时，系统才会执行flush操作，将缓冲区中的数据冲洗至硬盘中；</li>
</ol>
<p>这种优化机制虽然提高了性能，但也给程序的写入操作带来了不确定性。</p>
<ol>
<li>对于AOF这样的持久化功能来说，冲洗机制将直接影响AOF持久化的安全性；</li>
<li>为了消除上述机制的不确定性，Redis向用户提供了appendfsync选项，来控制系统冲洗AOF的频率；</li>
<li>Linux的glibc提供了fsync函数，可以将指定文件强制从缓冲区刷到硬盘，上述选项正是基于此函数。</li>
</ol>
<p>appendfsync选项的取值和含义如下：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-6.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h5 id="AOF持久化的优缺点如下：">AOF持久化的优缺点如下：</h5>
<ul>
<li>优点：与RDB持久化可能丢失大量的数据相比，AOF持久化的安全性要高很多。通过使用everysec选项，用户可以将数据丢失的时间窗口限制在1秒之内。</li>
<li>缺点：AOF文件存储的是协议文本，它的体积要比二进制格式的”.rdb”文件大很多。AOF需要通过执行AOF文件中的命令来恢复数据库，其恢复速度比RDB慢很多。AOF在进行重写时也需要创建子进程，在数据库体积较大时将占用大量资源，会导致服务器的短暂阻塞。</li>
</ul>
<h5 id="RDB-AOF混合持久化：">RDB-AOF混合持久化：</h5>
<p>Redis从4.0开始引入RDB-AOF混合持久化模式，这种模式是基于AOF持久化构建而来的。用户可以通过配置文件中的“aof-use-rdb-preamble yes”配置项开启AOF混合持久化。Redis服务器在执行AOF重写操作时，会按照如下原则处理数据：</p>
<ul>
<li>像执行BGSAVE命令一样，根据数据库当前的状态生成相应的RDB数据，并将其写入AOF文件中；</li>
<li>对于重写之后执行的Redis命令，则以协议文本的方式追加到AOF文件的末尾，即RDB数据之后。</li>
</ul>
<p>通过使用RDB-AOF混合持久化，用户可以同时获得RDB持久化和AOF持久化的优点，服务器既可以通过AOF文件包含的RDB数据来实现快速的数据恢复操作，又可以通过AOF文件包含的AOF数据来将丢失数据的时间窗口限制在1s之内。</p>
<h2 id="1-12-如何实现Redis的高可用？">1.12 如何实现Redis的高可用？</h2>
<p><strong>参考答案</strong></p>
<p>实现Redis的高可用，主要有<strong>哨兵</strong>和<strong>集群</strong>两种方式。</p>
<h3 id="哨兵：">哨兵：</h3>
<p>Redis Sentinel（哨兵）是一个分布式架构，它包含若干个哨兵节点和数据节点。每个哨兵节点会对数据节点和其余的哨兵节点进行监控，当发现节点不可达时，会对节点做下线标识。如果被标识的是主节点，它就会与其他的哨兵节点进行协商，当多数哨兵节点都认为主节点不可达时，它们便会选举出一个哨兵节点来完成自动故障转移的工作，同时还会将这个变化实时地通知给应用方。整个过程是自动的，不需要人工介入，有效地解决了Redis的高可用问题！</p>
<p>一组哨兵可以监控一个主节点，也可以同时监控多个主节点，两种情况的拓扑结构如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-7.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>哨兵节点包含如下的特征：</p>
<ol>
<li>哨兵节点会定期监控数据节点，其他哨兵节点是否可达；</li>
<li>哨兵节点会将故障转移的结果通知给应用方；</li>
<li>哨兵节点可以将从节点晋升为主节点，并维护后续正确的主从关系；</li>
<li>哨兵模式下，客户端连接的是哨兵节点集合，从中获取主节点信息；</li>
<li>节点的故障判断是由多个哨兵节点共同完成的，可有效地防止误判；</li>
<li>哨兵节点集合是由多个哨兵节点组成的，即使个别哨兵节点不可用，整个集合依然是健壮的；</li>
<li>哨兵节点也是独立的Redis节点，是特殊的Redis节点，它们不存储数据，只支持部分命令。</li>
</ol>
<h3 id="集群：">集群：</h3>
<p>Redis集群采用虚拟槽分区来实现数据分片，它把所有的键根据哈希函数映射到<code>0-16383</code>整数槽内，计算公式为<code>slot=CRC16(key)&amp;16383</code>，每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点：</p>
<ol>
<li>解耦数据和节点之间的关系，简化了节点扩容和收缩的难度；</li>
<li>节点自身维护槽的映射关系，不需要客户端或者代理服务维护槽分区元数据；</li>
<li>支持节点、槽、键之间的映射查询，用于数据路由，在线伸缩等场景。</li>
</ol>
<p>Redis集群中数据的分片逻辑如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-8.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h2 id="1-13-Redis的主从同步是如何实现的？">1.13 Redis的主从同步是如何实现的？</h2>
<p><strong>参考答案</strong></p>
<p>从2.8版本开始，Redis使用psync命令完成主从数据同步，同步过程分为全量复制和部分复制。**全量复制一般用于初次复制的场景，部分复制则用于处理因网络中断等原因造成数据丢失的场景。**psync命令需要以下参数的支持：</p>
<ol>
<li>复制偏移量：主节点处理写命令后，会把命令长度做累加记录，从节点在接收到写命令后，也会做累加记录；从节点会每秒钟上报一次自身的复制偏移量给主节点，而主节点则会保存从节点的复制偏移量。</li>
<li>积压缓冲区：保存在主节点上的一个固定长度的队列，默认大小为1M，当主节点有连接的从节点时被创建；主节点处理写命令时，不但会把命令发送给从节点，还会写入积压缓冲区；缓冲区是先进先出的队列，可以保存最近已复制的数据，用于部分复制和命令丢失的数据补救。</li>
<li>主节点运行ID：每个Redis节点启动后，都会动态分配一个40位的十六进制字符串作为运行ID；如果使用IP和端口的方式标识主节点，那么主节点重启变更了数据集（RDB/AOF），从节点再基于复制偏移量复制数据将是不安全的，因此当主节点的运行ID变化后，从节点将做全量复制。</li>
</ol>
<p>psync命令的执行过程以及返回结果，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-9.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>全量复制的过程，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-10.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>部分复制的过程，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-11.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h2 id="1-14-Redis为什么存的快，内存断电数据怎么恢复？">1.14 Redis为什么存的快，内存断电数据怎么恢复？</h2>
<p><strong>参考答案</strong></p>
<p><strong>Redis存的快是因为它的数据都存放在内存里，并且为了保证数据的安全性</strong>，Redis还提供了三种数据的持久化机制，即<strong>RDB持久化、AOF持久化、RDB-AOF混合持久化</strong>。若服务器断电，那么我们可以利用持久化文件，对数据进行恢复。理论上来说，AOF/RDB-AOF持久化可以将丢失数据的窗口控制在1S之内。</p>
<h2 id="1-15-说一说Redis的缓存淘汰策略">1.15 说一说Redis的缓存淘汰策略</h2>
<p><strong>参考答案</strong></p>
<p>当写入数据将导致超出maxmemory限制时，Redis会采用maxmemory-policy所指定的策略进行数据淘汰，该策略一共包含如下8种选项：</p>
<table>
<thead>
<tr>
<th style="text-align:center"><strong>策略</strong></th>
<th style="text-align:center"><strong>描述</strong></th>
<th style="text-align:center"><strong>版本</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">noeviction</td>
<td style="text-align:center">直接返回错误；</td>
<td style="text-align:center"></td>
</tr>
<tr>
<td style="text-align:center">volatile-ttl</td>
<td style="text-align:center">从设置了过期时间的键中，选择过期时间最小的键，进行淘汰；</td>
<td style="text-align:center"></td>
</tr>
<tr>
<td style="text-align:center">volatile-random</td>
<td style="text-align:center">从设置了过期时间的键中，随机选择键，进行淘汰；</td>
<td style="text-align:center"></td>
</tr>
<tr>
<td style="text-align:center">volatile-lru</td>
<td style="text-align:center">从设置了过期时间的键中，使用LRU算法选择键，进行淘汰；</td>
<td style="text-align:center"></td>
</tr>
<tr>
<td style="text-align:center">volatile-lfu</td>
<td style="text-align:center">从设置了过期时间的键中，使用LFU算法选择键，进行淘汰；</td>
<td style="text-align:center">4.0</td>
</tr>
<tr>
<td style="text-align:center">allleys-random</td>
<td style="text-align:center">从所有的键中，随机选择键，进行淘汰；</td>
<td style="text-align:center"></td>
</tr>
<tr>
<td style="text-align:center">allkeys-lru</td>
<td style="text-align:center">从所有的键中，使用LRU算法选择键，进行淘汰；</td>
<td style="text-align:center"></td>
</tr>
<tr>
<td style="text-align:center">allkeys-lfu</td>
<td style="text-align:center">从所有的键中，使用LFU算法选择键，进行淘汰；</td>
<td style="text-align:center">4.0</td>
</tr>
</tbody>
</table>
<p>其中，<strong>volatile前缀代表从设置了过期时间的键中淘汰数据</strong>，<strong>allkeys前缀代表从所有的键中淘汰数据</strong>。关于后缀，<strong>ttl代表选择过期时间最小的键</strong>，<strong>random代表随机选择键</strong>，需要我们额外关注的是lru和lfu后缀，它们分别代表采用lru算法和lfu算法来淘汰数据。</p>
<p><strong>LRU（Least Recently Used）是按照最近最少使用原则来筛选数据，即最不常用的数据会被筛选出来！</strong></p>
<ul>
<li>标准LRU：把所有的数据组成一个链表，表头和表尾分别表示MRU和LRU端，即最常使用端和最少使用端。刚被访问的数据会被移动到MRU端，而新增的数据也是刚被访问的数据，也会被移动到MRU端。当链表的空间被占满时，它会删除LRU端的数据。</li>
<li>近似LRU：Redis会记录每个数据的最近一次访问的时间戳（LRU）。Redis执行写入操作时，若发现内存超出maxmemory，就会执行一次近似LRU淘汰算法。近似LRU会随机采样N个key，然后淘汰掉最旧的key，若淘汰后内存依然超出限制，则继续采样淘汰。可以通过maxmemory_samples配置项，设置近似LRU每次采样的数据个数，该配置项的默认值为5。</li>
</ul>
<p>LRU算法的不足之处在于，若一个key很少被访问，只是刚刚偶尔被访问了一次，则它就被认为是热点数据，短时间内不会被淘汰。</p>
<p>LFU算法正式用于解决上述问题，LFU（Least Frequently Used， 最不经常使用算法、最少访问算法）是Redis4新增的淘汰策略，<strong>它根据key的最近访问频率进行淘汰</strong>。LFU在LRU的基础上，为每个数据增加了一个计数器，来统计这个数据的访问次数。当使用LFU策略淘汰数据时，首先会根据数据的访问次数进行筛选，把访问次数最低的数据淘汰出内存。如果两个数据的访问次数相同，LFU再比较这两个数据的访问时间，把访问时间更早的数据淘汰出内存。</p>
<h2 id="1-16-请介绍一下Redis的过期策略">1.16 请介绍一下Redis的过期策略</h2>
<p><strong>参考答案</strong></p>
<h3 id="定时删除">定时删除</h3>
<ul>
<li>创建一个定时器，当key设置有过期时间，且过期时间到达时，由定时器任务<strong>立即执行</strong>对键的删除操作</li>
<li>优点：<strong>节约内存</strong>，到时就删除，快速释放掉不必要的内存占用</li>
<li>缺点：<strong>CPU压力很大</strong>，无论CPU此时负载量多高，均占用CPU，会影响redis服务器响应时间和指令吞吐量</li>
<li>总结：用处理器性能换取存储空间 （<strong>拿时间换空间</strong>）</li>
</ul>
<h3 id="惰性删除">惰性删除</h3>
<ul>
<li>数据到达过期时间，不做处理。等下次访问该数据时
<ul>
<li>如果未过期，返回数据</li>
<li>发现已过期，删除，返回不存在</li>
</ul>
</li>
<li>优点：<strong>节约CPU性能</strong>，发现必须删除的时候才删除</li>
<li>缺点：<strong>内存压力很大</strong>，出现长期占用内存的数据</li>
<li>总结：用存储空间换取处理器性能 （拿空间换时间）</li>
</ul>
<h3 id="定期删除">定期删除</h3>
<p><a target="_blank" rel="noopener" href="https://igotcha.gitee.io/2020/08/10/Redis/Redis%E5%9F%BA%E7%A1%80/Redis%E5%9F%BA%E7%A1%80/8.2.png"><img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/8.2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img"></a></p>
<ul>
<li>周期性轮询redis库中的时效性数据，采用<strong>随机抽取的策略</strong>，利用过期数据占比的方式控制删除频度</li>
<li>特点1：CPU性能占用设置有峰值，检测频度可自定义设置</li>
<li>特点2：内存压力不是很大，长期占用内存的冷数据会被持续清理</li>
<li>总结：周期性抽查存储空间 （随机抽查，重点抽查）</li>
</ul>
<h2 id="1-17-缓存穿透、缓存击穿、缓存雪崩有什么区别，该如何解决？">1.17 缓存穿透、缓存击穿、缓存雪崩有什么区别，该如何解决？</h2>
<p><strong>参考答案</strong></p>
<h3 id="缓存穿透：">缓存穿透：</h3>
<p>问题描述：</p>
<p>客户端查询根本不存在的数据，使得请求直达存储层，导致其负载过大，甚至宕机。出现这种情况的原因，可能是业务层误将缓存和库中的数据删除了，也可能是有人恶意攻击，专门访问库中不存在的数据。</p>
<p>解决方案：</p>
<ol>
<li>缓存空对象：存储层未命中后，仍然将空值存入缓存层，客户端再次访问数据时，缓存层会直接返回空值。</li>
<li>布隆过滤器：将数据存入布隆过滤器，访问缓存之前以过滤器拦截，若请求的数据不存在则直接返回空值。</li>
</ol>
<h3 id="缓存击穿：">缓存击穿：</h3>
<p>问题描述：</p>
<p>一份热点数据，它的访问量非常大。在其缓存失效的瞬间，大量请求直达存储层，导致服务崩溃。</p>
<p>解决方案：</p>
<ol>
<li>永不过期：热点数据不设置过期时间，所以不会出现上述问题，这是“物理”上的永不过期。或者为每个数据设置逻辑过期时间，当发现该数据逻辑过期时，使用单独的线程重建缓存。</li>
<li>加互斥锁：对数据的访问加互斥锁，当一个线程访问该数据时，其他线程只能等待。这个线程访问过后，缓存中的数据将被重建，届时其他线程就可以直接从缓存中取值。</li>
</ol>
<h3 id="缓存雪崩：">缓存雪崩：</h3>
<p>问题描述：</p>
<p>在某一时刻，缓存层无法继续提供服务，导致所有的请求直达存储层，造成数据库宕机。可能是缓存中有大量数据同时过期，也可能是Redis节点发生故障，导致大量请求无法得到处理。</p>
<p>解决方案：</p>
<ol>
<li>避免数据同时过期：设置过期时间时，附加一个随机数，避免大量的key同时过期。</li>
<li>启用降级和熔断措施：在发生雪崩时，若应用访问的不是核心数据，则直接返回预定义信息/空值/错误信息。或者在发生雪崩时，对于访问缓存接口的请求，客户端并不会把请求发给Redis，而是直接返回。</li>
<li>构建高可用的Redis服务：采用哨兵或集群模式，部署多个Redis实例，个别节点宕机，依然可以保持服务的整体可用。</li>
</ol>
<h2 id="1-18-如何保证缓存与数据库的双写一致性？">1.18 如何保证缓存与数据库的双写一致性？</h2>
<p><strong>参考答案</strong></p>
<p>四种同步策略：</p>
<p>想要保证缓存与数据库的双写一致，一共有4种方式，即4种同步策略：</p>
<ol>
<li>先更新缓存，再更新数据库；</li>
<li>先更新数据库，再更新缓存；</li>
<li>先删除缓存，再更新数据库；</li>
<li>先更新数据库，再删除缓存。</li>
</ol>
<p>从这4种同步策略中，我们需要作出比较的是：</p>
<ol>
<li>更新缓存与删除缓存哪种方式更合适？</li>
<li>应该先操作数据库还是先操作缓存？</li>
</ol>
<p>更新缓存还是删除缓存：</p>
<p>下面，我们来分析一下，应该采用更新缓存还是删除缓存的方式。</p>
<ul>
<li>
<p>更新缓存</p>
<p>优点：每次数据变化都及时更新缓存，所以查询时不容易出现未命中的情况。</p>
<p>缺点：更新缓存的消耗比较大。如果数据需要经过复杂的计算再写入缓存，那么频繁的更新缓存，就会影响服务器的性能。如果是写入数据频繁的业务场景，那么可能频繁的更新缓存时，却没有业务读取该数据。</p>
</li>
<li>
<p>删除缓存</p>
<p>优点：操作简单，无论更新操作是否复杂，都是将缓存中的数据直接删除。</p>
<p>缺点：删除缓存后，下一次查询缓存会出现未命中，这时需要重新读取一次数据库。</p>
</li>
</ul>
<p>从上面的比较来看，一般情况下，删除缓存是更优的方案。</p>
<p>先操作数据库还是缓存：</p>
<p>下面，我们再来分析一下，应该先操作数据库还是先操作缓存。</p>
<p>首先，我们将先删除缓存与先更新数据库，在出现失败时进行一个对比：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/cache-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>如上图，是先删除缓存再更新数据库，在出现失败时可能出现的问题：</p>
<ol>
<li>进程A删除缓存成功；</li>
<li>进程A更新数据库失败；</li>
<li>进程B从缓存中读取数据；</li>
<li>由于缓存被删，进程B无法从缓存中得到数据，进而从数据库读取数据；</li>
<li>进程B从数据库成功获取数据，然后将数据更新到了缓存。</li>
</ol>
<p>最终，缓存和数据库的数据是一致的，但仍然是旧的数据。而我们的期望是二者数据一致，并且是新的数据。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/cache-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>如上图，是先更新数据库再删除缓存，在出现失败时可能出现的问题：</p>
<ol>
<li>进程A更新数据库成功；</li>
<li>进程A删除缓存失败；</li>
<li>进程B读取缓存成功，由于缓存删除失败，所以进程B读取到的是旧的数据。</li>
</ol>
<p>最终，缓存和数据库的数据是不一致的。</p>
<p>经过上面的比较，我们发现在出现失败的时候，是无法明确分辨出先删缓存和先更新数据库哪个方式更好，以为它们都存在问题。后面我们会进一步对这两种方式进行比较，但是在这里我们先探讨一下，上述场景出现的问题，应该如何解决呢？</p>
<p>实际上，无论上面我们采用哪种方式去同步缓存与数据库，在第二步出现失败的时候，都建议采用重试机制解决，因为最终我们是要解决掉这个错误的。而为了避免重试机制影响主要业务的执行，一般建议重试机制采用异步的方式执行，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/cache-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>这里我们按照先更新数据库，再删除缓存的方式，来说明重试机制的主要步骤：</p>
<ol>
<li>更新数据库成功；</li>
<li>删除缓存失败；</li>
<li>将此数据加入消息队列；</li>
<li>业务代码消费这条消息；</li>
<li>业务代码根据这条消息的内容，发起重试机制，即从缓存中删除这条记录。</li>
</ol>
<p>好了，下面我们再将先删缓存与先更新数据库，在没有出现失败时进行对比：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/cache-4.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>如上图，是先删除缓存再更新数据库，在没有出现失败时可能出现的问题：</p>
<ol>
<li>进程A删除缓存成功；</li>
<li>进程B读取缓存失败；</li>
<li>进程B读取数据库成功，得到旧的数据；</li>
<li>进程B将旧的数据成功地更新到了缓存；</li>
<li>进程A将新的数据成功地更新到数据库。</li>
</ol>
<p>可见，进程A的两步操作均成功，但由于存在并发，在这两步之间，进程B访问了缓存。最终结果是，缓存中存储了旧的数据，而数据库中存储了新的数据，二者数据不一致。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/cache-5.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>如上图，是先更新数据库再删除缓存，再没有出现失败时可能出现的问题：</p>
<ol>
<li>进程A更新数据库成功；</li>
<li>进程B读取缓存成功；</li>
<li>进程A更新数据库成功。</li>
</ol>
<p>可见，最终缓存与数据库的数据是一致的，并且都是最新的数据。但进程B在这个过程里读到了旧的数据，可能还有其他进程也像进程B一样，在这两步之间读到了缓存中旧的数据，但因为这两步的执行速度会比较快，所以影响不大。对于这两步之后，其他进程再读取缓存数据的时候，就不会出现类似于进程B的问题了。</p>
<p>最终结论：</p>
<p>经过对比你会发现，先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况，则可以采用重试机制解决问题。</p>
<p><strong>扩展阅读</strong></p>
<h3 id="延时双删">延时双删</h3>
<p>上面我们提到，如果是先删缓存、再更新数据库，在没有出现失败时可能会导致数据的不一致。如果在实际的应用中，出于某些考虑我们需要选择这种方式，那有办法解决这个问题吗？答案是有的，那就是采用延时双删的策略，延时双删的基本思路如下：</p>
<ol>
<li>删除缓存；</li>
<li>更新数据库；</li>
<li>sleep N毫秒；</li>
<li>再次删除缓存。</li>
</ol>
<p>**阻塞一段时间之后，再次删除缓存，就可以把这个过程中缓存中不一致的数据删除掉。**而具体的时间，要评估你这项业务的大致时间，按照这个时间来设定即可。</p>
<h2 id="1-19-请介绍Redis集群的实现方案">1.19 请介绍Redis集群的实现方案</h2>
<p><strong>参考答案</strong></p>
<h3 id="Redis集群的分区方案：">Redis集群的分区方案：</h3>
<p>Redis集群采用虚拟槽分区来实现数据分片，它把所有的键根据哈希函数映射到<code>0-16383</code>整数槽内，计算公式为<code>slot=CRC16(key)&amp;16383</code>，每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点：</p>
<ol>
<li>解耦数据和节点之间的关系，简化了节点扩容和收缩的难度；</li>
<li>节点自身维护槽的映射关系，不需要客户端或者代理服务维护槽分区元数据；</li>
<li>支持节点、槽、键之间的映射查询，用于数据路由，在线伸缩等场景。</li>
</ol>
<h3 id="Redis集群中数据的分片逻辑如下图：">Redis集群中数据的分片逻辑如下图：</h3>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-8-163591381262734.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h3 id="Redis集群的功能限制：">Redis集群的功能限制：</h3>
<p>Redis集群方案在扩展了Redis处理能力的同时，也带来了一些使用上的限制：</p>
<ol>
<li>key批量操作支持有限。如mset、mget，目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mset、mget等操作可能存在于多个节点上所以不被支持。</li>
<li>key事务操作支持有限。同理只支持多key在同一节点上的事务操作，当多个key分布在不同的节点上时无法使用事务功能。</li>
<li>key作为数据分区的最小粒度，因此不能将一个大的键值对象（如hash、list等）映射到不同的节点。</li>
<li>不支持多数据库空间。单机下的Redis可以支持16个数据库，集群模式下只能使用一个数据库空间，即DB0。</li>
<li>复制结构只支持一层，从节点只能复制主节点，不支持嵌套树状复制结构。</li>
</ol>
<h3 id="Redis集群的通信方案：">Redis集群的通信方案：</h3>
<p>在分布式存储中需要提供维护节点元数据信息的机制，所谓元数据是指：节点负责哪些数据，是否出现故障等状态信息。常见的元数据维护方式分为：集中式和P2P方式。</p>
<p>Redis集群采用P2P的Gossip（流言）协议，Gossip协议的工作原理就是节点彼此不断通信交换信息，一段时间后所有的节点都会知道集群完整的信息，这种方式类似流言传播。通信的大致过程如下：</p>
<ol>
<li>集群中每个节点都会单独开辟一个TCP通道，用于节点之间彼此通信，通信端口号在基础端口号上加10000；</li>
<li>每个节点再固定周期内通过特定规则选择几个节点发送ping消息；</li>
<li>接收ping消息的节点用pong消息作为响应。</li>
</ol>
<p>其中，Gossip协议的主要职责就是信息交换，而信息交换的载体就是节点彼此发送的Gossip消息，Gossip消息分为：meet消息、ping消息、pong消息、fail消息等。</p>
<ul>
<li>meet消息：用于通知新节点加入，消息发送者通知接受者加入到当前集群。meet消息通信正常完成后，接收节点会加入到集群中并进行周期性的ping、pong消息交换。</li>
<li>ping消息：集群内交换最频繁的消息，集群内每个节点每秒向多个其他节点发送ping消息，用于检测节点是否在线和交换彼此状态信息。ping消息封装了自身节点和一部分其他节点的状态数据。</li>
<li>pong消息：当接收到meet、ping消息时，作为响应消息回复给发送方确认消息正常通信。pong消息内封装了自身状态数据，节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。</li>
<li>fail消息：当节点判定集群内另一个节点下线时，会向集群内广播一个fail消息，其他节点接收到fail消息之后把对应节点更新为下线状态。</li>
</ul>
<p>虽然Gossip协议的信息交换机制具有天然的分布式特性，但它是有成本的。因为Redis集群内部需要频繁地进行节点信息交换，而ping/pong消息会携带当前节点和部分其他节点的状态数据，势必会加重带宽和计算的负担。所以，Redis集群的Gossip协议需要兼顾信息交换的实时性和成本的开销。</p>
<ul>
<li>集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点，然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息，以此来检测被选中的节点是否在线。</li>
<li>如果节点A最后一次收到节点B发送的PONG消息的时间，距离当前时间已经超过了节点A的超时选项设置时长的一半（cluster-node-timeout/2），那么节点A也会向节点B发送PING消息，这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。</li>
<li>每个消息主要的数据占用：slots槽数组（2KB）和整个集群1/10的状态数据（10个节点状态数据约1KB）。</li>
</ul>
<h2 id="1-21-说一说Redis集群的应用和优劣势">1.21 说一说Redis集群的应用和优劣势</h2>
<p><strong>参考答案</strong></p>
<h3 id="优势：">优势：</h3>
<p>Redis Cluster是Redis的分布式解决方案，在3.0版本正式推出，有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时，可以采用Cluster架构方案达到负载均衡的目的。</p>
<h4 id="劣势：">劣势：</h4>
<p>Redis集群方案在扩展了Redis处理能力的同时，也带来了一些使用上的限制：</p>
<ol>
<li>key批量操作支持有限。如mset、mget，目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mset、mget等操作可能存在于多个节点上所以不被支持。</li>
<li>key事务操作支持有限。同理只支持多key在同一节点上的事务操作，当多个key分布在不同的节点上时无法使用事务功能。</li>
<li>key作为数据分区的最小粒度，因此不能将一个大的键值对象（如hash、list等）映射到不同的节点。</li>
<li>不支持多数据库空间。单机下的Redis可以支持16个数据库，集群模式下只能使用一个数据库空间，即DB0。</li>
<li>复制结构只支持一层，从节点只能复制主节点，不支持嵌套树状复制结构。</li>
</ol>
<h2 id="1-22-说一说hash类型底层的数据结构">1.22 说一说hash类型底层的数据结构</h2>
<p><strong>参考答案</strong></p>
<p>哈希对象有两种编码方案，当同时满足以下条件时，哈希对象采用ziplist编码，否则采用hashtable编码：</p>
<ul>
<li>哈希对象保存的键值对数量小于512个；</li>
<li>哈希对象保存的所有键值对中的键和值，其字符串长度都小于64字节。</li>
</ul>
<p>其中，ziplist编码采用压缩列表作为底层实现，而hashtable编码采用字典作为底层实现。</p>
<h3 id="压缩列表：">压缩列表：</h3>
<p>压缩列表（ziplist），是Redis为了节约内存而设计的一种线性数据结构，它是由一系列具有特殊编码的连续内存块构成的。一个压缩列表可以包含任意多个节点，每个节点可以保存一个字节数组或一个整数值。</p>
<p>压缩列表的结构如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-hash-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>该结构当中的字段含义如下表所示：</p>
<table>
<thead>
<tr>
<th style="text-align:center"><strong>属性</strong></th>
<th style="text-align:center"><strong>类型</strong></th>
<th style="text-align:center"><strong>长度</strong></th>
<th style="text-align:center"><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">zlbytes</td>
<td style="text-align:center">uint32_t</td>
<td style="text-align:center">4字节</td>
<td style="text-align:center">压缩列表占用的内存字节数；</td>
</tr>
<tr>
<td style="text-align:center">zltail</td>
<td style="text-align:center">uint32_t</td>
<td style="text-align:center">4字节</td>
<td style="text-align:center">压缩列表表尾节点距离列表起始地址的偏移量（单位字节）；</td>
</tr>
<tr>
<td style="text-align:center">zllen</td>
<td style="text-align:center">uint16_t</td>
<td style="text-align:center">2字节</td>
<td style="text-align:center">压缩列表包含的节点数量，等于UINT16_MAX时，需遍历列表计算真实数量；</td>
</tr>
<tr>
<td style="text-align:center">entryX</td>
<td style="text-align:center">列表节点</td>
<td style="text-align:center">不固定</td>
<td style="text-align:center">压缩列表包含的节点，节点的长度由节点所保存的内容决定；</td>
</tr>
<tr>
<td style="text-align:center">zlend</td>
<td style="text-align:center">uint8_t</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">压缩列表的结尾标识，是一个固定值0xFF；</td>
</tr>
</tbody>
</table>
<p>其中，压缩列表的节点由以下字段构成：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-hash-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>previous_entry_length（pel）属性以字节为单位，记录当前节点的前一节点的长度，其自身占据1字节或5字节：</p>
<ol>
<li>如果前一节点的长度小于254字节，则“pel”属性的长度为1字节，前一节点的长度就保存在这一个字节内；</li>
<li>如果前一节点的长度达到254字节，则“pel”属性的长度为5字节，其中第一个字节被设置为0xFE，之后的四个字节用来保存前一节点的长度；</li>
</ol>
<p>基于“pel”属性，程序便可以通过指针运算，根据当前节点的起始地址计算出前一节点的起始地址，从而实现从表尾向表头的遍历操作。</p>
<p>content属性负责保存节点的值（字节数组或整数），其类型和长度则由encoding属性决定，它们的关系如下：</p>
<table>
<thead>
<tr>
<th style="text-align:center"><strong>encoding</strong></th>
<th style="text-align:center"><strong>长度</strong></th>
<th style="text-align:center"><strong>content</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">00 xxxxxx</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">最大长度为26 -1的字节数组；</td>
</tr>
<tr>
<td style="text-align:center">01 xxxxxx bbbbbbbb</td>
<td style="text-align:center">2字节</td>
<td style="text-align:center">最大长度为214-1的字节数组；</td>
</tr>
<tr>
<td style="text-align:center">10 <strong>__</strong> bbbbbbbb … … …</td>
<td style="text-align:center">5字节</td>
<td style="text-align:center">最大长度为232-1的字节数组；</td>
</tr>
<tr>
<td style="text-align:center">11 000000</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">int16_t类型的整数；</td>
</tr>
<tr>
<td style="text-align:center">11 010000</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">int32_t类型的整数；</td>
</tr>
<tr>
<td style="text-align:center">11 100000</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">int64_t类型的整数；</td>
</tr>
<tr>
<td style="text-align:center">11 110000</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">24位有符号整数；</td>
</tr>
<tr>
<td style="text-align:center">11 111110</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">8位有符号整数；</td>
</tr>
<tr>
<td style="text-align:center">11 11xxxx</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">没有content属性，xxxx直接存[0,12]范围的整数值；</td>
</tr>
</tbody>
</table>
<h3 id="字典：">字典：</h3>
<p>字典（dict）又称为散列表，是一种用来存储键值对的数据结构。C语言没有内置这种数据结构，所以Redis构建了自己的字典实现。</p>
<p>Redis字典的实现主要涉及三个结构体：字典、哈希表、哈希表节点。其中，每个哈希表节点保存一个键值对，每个哈希表由多个哈希表节点构成，而字典则是对哈希表的进一步封装。这三个结构体的关系如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-hash-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>其中，dict代表字典，dictht代表哈希表，dictEntry代表哈希表节点。可以看出，dictEntry是一个数组，这很好理解，因为一个哈希表里要包含多个哈希表节点。而dict里包含2个dictht，多出的哈希表用于REHASH。当哈希表保存的键值对数量过多或过少时，需要对哈希表的大小进行扩展或收缩操作，在Redis中，扩展和收缩哈希表是通过REHASH实现的，执行REHASH的大致步骤如下：</p>
<ol>
<li>
<p>为字典的ht[1]哈希表分配内存空间</p>
<p>如果执行的是扩展操作，则ht[1]的大小为第1个大于等于ht[0].used*2的2n。如果执行的是收缩操作，则ht[1]的大小为第1个大于等于ht[0].used的2n。</p>
</li>
<li>
<p>将存储在ht[0]中的数据迁移到ht[1]上</p>
<p>重新计算键的哈希值和索引值，然后将键值对放置到ht[1]哈希表的指定位置上。</p>
</li>
<li>
<p>将字典的ht[1]哈希表晋升为默认哈希表</p>
<p>迁移完成后，清空ht[0]，再交换ht[0]和ht[1]的值，为下一次REHASH做准备。</p>
</li>
</ol>
<p>当满足以下任何一个条件时，程序会自动开始对哈希表执行扩展操作：</p>
<ol>
<li>服务器目前没有执行bgsave或bgrewriteof命令，并且哈希表的负载因子大于等于1；</li>
<li>服务器目前正在执行bgsave或bgrewriteof命令，并且哈希表的负载因子大于等于5。</li>
</ol>
<p>为了避免REHASH对服务器性能造成影响，REHASH操作不是一次性地完成的，而是分多次、渐进式地完成的。渐进式REHASH的详细过程如下：</p>
<ol>
<li>为ht[1]分配空间，让字典同时持有ht[0]和ht[1]两个哈希表；</li>
<li>在字典中的索引计数器rehashidx设置为0，表示REHASH操作正式开始；</li>
<li>在REHASH期间，每次对字典执行添加、删除、修改、查找操作时，程序除了执行指定的操作外，还会顺带将ht[0]中位于rehashidx上的所有键值对迁移到ht[1]中，再将rehashidx的值加1；</li>
<li>随着字典不断被访问，最终在某个时刻，ht[0]上的所有键值对都被迁移到ht[1]上，此时程序将rehashidx属性值设置为-1，标识REHASH操作完成。</li>
</ol>
<p>REHSH期间，字典同时持有两个哈希表，此时的访问将按照如下原则处理：</p>
<ol>
<li>新添加的键值对，一律被保存到ht[1]中；</li>
<li>删除、修改、查找等其他操作，会在两个哈希表上进行，即程序先尝试去ht[0]中访问要操作的数据，若不存在则到ht[1]中访问，再对访问到的数据做相应的处理。</li>
</ol>
<h2 id="1-23-介绍一下zset类型底层的数据结构（有序集合）">1.23 介绍一下zset类型底层的数据结构（有序集合）</h2>
<p><strong>参考答案</strong></p>
<p>有序集合对象有2种编码方案，当同时满足以下条件时，集合对象采用ziplist编码，否则采用skiplist编码：</p>
<ul>
<li>有序集合保存的元素数量不超过128个；</li>
<li>有序集合保存的所有元素的成员长度都小于64字节。</li>
</ul>
<p>其中，ziplist编码的有序集合采用压缩列表作为底层实现，skiplist编码的有序集合采用zset结构作为底层实现。</p>
<p>其中，<strong>zset是一个复合结构，它的内部采用字典和跳跃表来实现</strong>，其源码如下。其中，dict保存了从成员到分支的映射关系，zsl则按分值由小到大保存了所有的集合元素。这样，当按照成员来访问有序集合时可以直接从dict中取值，当按照分值的范围访问有序集合时可以直接从zsl中取值，采用了空间换时间的策略以提高访问效率。</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs c"><span class="hljs-keyword">typedef</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">zset</span> &#123;</span><br>    dict *dict;        <span class="hljs-comment">// 字典，保存了从成员到分值的映射关系；</span><br>    zskiplist *zsl; <span class="hljs-comment">// 跳跃表，按分值由小到大保存所有集合元素；</span><br>&#125; zset;<br></code></pre></td></tr></table></figure>
<h3 id="综上，zset对象的底层数据结构包括：压缩列表、字典、跳跃表。">综上，<strong>zset对象的底层数据结构包括：压缩列表、字典、跳跃表。</strong></h3>
<h3 id="压缩列表：-2">压缩列表：</h3>
<p>压缩列表（ziplist），是Redis为了节约内存而设计的一种线性数据结构，它是由一系列具有特殊编码的连续内存块构成的。一个压缩列表可以包含任意多个节点，每个节点可以保存一个字节数组或一个整数值。</p>
<p>压缩列表的结构如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-hash-1-163591393832639.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>该结构当中的字段含义如下表所示：</p>
<table>
<thead>
<tr>
<th style="text-align:center"><strong>属性</strong></th>
<th style="text-align:center"><strong>类型</strong></th>
<th style="text-align:center"><strong>长度</strong></th>
<th style="text-align:center"><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">zlbytes</td>
<td style="text-align:center">uint32_t</td>
<td style="text-align:center">4字节</td>
<td style="text-align:center">压缩列表占用的内存字节数；</td>
</tr>
<tr>
<td style="text-align:center">zltail</td>
<td style="text-align:center">uint32_t</td>
<td style="text-align:center">4字节</td>
<td style="text-align:center">压缩列表表尾节点距离列表起始地址的偏移量（单位字节）；</td>
</tr>
<tr>
<td style="text-align:center">zllen</td>
<td style="text-align:center">uint16_t</td>
<td style="text-align:center">2字节</td>
<td style="text-align:center">压缩列表包含的节点数量，等于UINT16_MAX时，需遍历列表计算真实数量；</td>
</tr>
<tr>
<td style="text-align:center">entryX</td>
<td style="text-align:center">列表节点</td>
<td style="text-align:center">不固定</td>
<td style="text-align:center">压缩列表包含的节点，节点的长度由节点所保存的内容决定；</td>
</tr>
<tr>
<td style="text-align:center">zlend</td>
<td style="text-align:center">uint8_t</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">压缩列表的结尾标识，是一个固定值0xFF；</td>
</tr>
</tbody>
</table>
<p>其中，压缩列表的节点由以下字段构成：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-hash-2-163591393832641.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>previous_entry_length（pel）属性以字节为单位，记录当前节点的前一节点的长度，其自身占据1字节或5字节：</p>
<ol>
<li>如果前一节点的长度小于254字节，则“pel”属性的长度为1字节，前一节点的长度就保存在这一个字节内；</li>
<li>如果前一节点的长度达到254字节，则“pel”属性的长度为5字节，其中第一个字节被设置为0xFE，之后的四个字节用来保存前一节点的长度；</li>
</ol>
<p>基于“pel”属性，程序便可以通过指针运算，根据当前节点的起始地址计算出前一节点的起始地址，从而实现从表尾向表头的遍历操作。</p>
<p>content属性负责保存节点的值（字节数组或整数），其类型和长度则由encoding属性决定，它们的关系如下：</p>
<table>
<thead>
<tr>
<th style="text-align:center"><strong>encoding</strong></th>
<th style="text-align:center"><strong>长度</strong></th>
<th style="text-align:center"><strong>content</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">00 xxxxxx</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">最大长度为26 -1的字节数组；</td>
</tr>
<tr>
<td style="text-align:center">01 xxxxxx bbbbbbbb</td>
<td style="text-align:center">2字节</td>
<td style="text-align:center">最大长度为214-1的字节数组；</td>
</tr>
<tr>
<td style="text-align:center">10 <strong>__</strong> bbbbbbbb … … …</td>
<td style="text-align:center">5字节</td>
<td style="text-align:center">最大长度为232-1的字节数组；</td>
</tr>
<tr>
<td style="text-align:center">11 000000</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">int16_t类型的整数；</td>
</tr>
<tr>
<td style="text-align:center">11 010000</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">int32_t类型的整数；</td>
</tr>
<tr>
<td style="text-align:center">11 100000</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">int64_t类型的整数；</td>
</tr>
<tr>
<td style="text-align:center">11 110000</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">24位有符号整数；</td>
</tr>
<tr>
<td style="text-align:center">11 111110</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">8位有符号整数；</td>
</tr>
<tr>
<td style="text-align:center">11 11xxxx</td>
<td style="text-align:center">1字节</td>
<td style="text-align:center">没有content属性，xxxx直接存[0,12]范围的整数值；</td>
</tr>
</tbody>
</table>
<h3 id="字典：-2">字典：</h3>
<p>字典（dict）又称为散列表，是一种用来存储键值对的数据结构。C语言没有内置这种数据结构，所以Redis构建了自己的字典实现。</p>
<p>Redis字典的实现主要涉及三个结构体：字典、哈希表、哈希表节点。其中，每个哈希表节点保存一个键值对，每个哈希表由多个哈希表节点构成，而字典则是对哈希表的进一步封装。这三个结构体的关系如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-hash-3-163591393832743.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>其中，dict代表字典，dictht代表哈希表，dictEntry代表哈希表节点。可以看出，dictEntry是一个数组，这很好理解，因为一个哈希表里要包含多个哈希表节点。而dict里包含2个dictht，多出的哈希表用于REHASH。当哈希表保存的键值对数量过多或过少时，需要对哈希表的大小进行扩展或收缩操作，在Redis中，扩展和收缩哈希表是通过REHASH实现的，执行REHASH的大致步骤如下：</p>
<ol>
<li>
<p>为字典的ht[1]哈希表分配内存空间</p>
<p>如果执行的是扩展操作，则ht[1]的大小为第1个大于等于ht[0].used*2的2n。如果执行的是收缩操作，则ht[1]的大小为第1个大于等于ht[0].used的2n。</p>
</li>
<li>
<p>将存储在ht[0]中的数据迁移到ht[1]上</p>
<p>重新计算键的哈希值和索引值，然后将键值对放置到ht[1]哈希表的指定位置上。</p>
</li>
<li>
<p>将字典的ht[1]哈希表晋升为默认哈希表</p>
<p>迁移完成后，清空ht[0]，再交换ht[0]和ht[1]的值，为下一次REHASH做准备。</p>
</li>
</ol>
<p>当满足以下任何一个条件时，程序会自动开始对哈希表执行扩展操作：</p>
<ol>
<li>服务器目前没有执行bgsave或bgrewriteof命令，并且哈希表的负载因子大于等于1；</li>
<li>服务器目前正在执行bgsave或bgrewriteof命令，并且哈希表的负载因子大于等于5。</li>
</ol>
<p>为了避免REHASH对服务器性能造成影响，REHASH操作不是一次性地完成的，而是分多次、渐进式地完成的。渐进式REHASH的详细过程如下：</p>
<ol>
<li>为ht[1]分配空间，让字典同时持有ht[0]和ht[1]两个哈希表；</li>
<li>在字典中的索引计数器rehashidx设置为0，表示REHASH操作正式开始；</li>
<li>在REHASH期间，每次对字典执行添加、删除、修改、查找操作时，程序除了执行指定的操作外，还会顺带将ht[0]中位于rehashidx上的所有键值对迁移到ht[1]中，再将rehashidx的值加1；</li>
<li>随着字典不断被访问，最终在某个时刻，ht[0]上的所有键值对都被迁移到ht[1]上，此时程序将rehashidx属性值设置为-1，标识REHASH操作完成。</li>
</ol>
<p>REHSH期间，字典同时持有两个哈希表，此时的访问将按照如下原则处理：</p>
<ol>
<li>新添加的键值对，一律被保存到ht[1]中；</li>
<li>删除、修改、查找等其他操作，会在两个哈希表上进行，即程序先尝试去ht[0]中访问要操作的数据，若不存在则到ht[1]中访问，再对访问到的数据做相应的处理。</li>
</ol>
<h3 id="跳跃表：">跳跃表：</h3>
<p>**跳跃表的查找复杂度为平均O(logN)，最坏O(N)，**效率堪比红黑树，却远比红黑树实现简单。跳跃表是在链表的基础上，通过增加索引来提高查找效率的。</p>
<p>有序链表插入、删除的复杂度为O(1)，而查找的复杂度为O(N)。例：若要查找值为60的元素，需要从第1个元素依次向后比较，共需比较6次才行，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-skiplist-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>跳跃表是从有序链表中选取部分节点，组成一个新链表，并以此作为原始链表的一级索引。再从一级索引中选取部分节点，组成一个新链表，并以此作为原始链表的二级索引。以此类推，可以有多级索引，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-skiplist-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>跳跃表在查找时，优先从高层开始查找，若next节点值大于目标值，或next指针指向NULL，则从当前节点下降一层继续向后查找，这样便可以提高查找的效率了。</p>
<p>跳跃表的实现主要涉及2个结构体：zskiplist、zskiplistNode，它们的关系如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-skiplist-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>其中，蓝色的表格代表zskiplist，红色的表格代表zskiplistNode。zskiplist有指向头尾节点的指针，以及列表的长度，列表中最高的层级。zskiplistNode的头节点是空的，它不存储任何真实的数据，它拥有最高的层级，但这个层级不记录在zskiplist之内。</p>
<h2 id="1-24-如何利用Redis实现分布式Session？">1.24 如何利用Redis实现分布式Session？</h2>
<p><strong>参考答案</strong></p>
<p>在web开发中，我们会把用户的登录信息存储在session里。而session是依赖于cookie的，即服务器创建session时会给它分配一个唯一的ID，并且在响应时创建一个cookie用于存储这个SESSIONID。当客户端收到这个cookie之后，就会自动保存这个SESSIONID，并且在下次访问时自动携带这个SESSIONID，届时服务器就可以通过这个SESSIONID得到与之对应的session，从而识别用户的身。如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/session-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>现在的互联网应用，基本都是采用分布式部署方式，即将应用程序部署在多台服务器上，并通过nginx做统一的请求分发。而服务器与服务器之间是隔离的，它们的session是不共享的，这就存在session同步的问题了，如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/session-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>如果客户端第一次访问服务器，请求被分发到了服务器A上，则服务器A会为该客户端创建session。如果客户端再次访问服务器，请求被分发到服务器B上，则由于服务器B中没有这个session，所以用户的身份无法得到验证，从而产生了不一致的问题。</p>
<p>解决这个问题的办法有很多，比如可以协调多个服务器，让他们的session保持同步。也可以在分发请求时做绑定处理，即将某一个IP固定分配给同一个服务器。但这些方式都比较麻烦，而且性能上也有一定的消耗。更合理的方式就是采用类似于Redis这样的高性能缓存服务器，来实现分布式session。</p>
<p>从上面的叙述可知，我们使用session保存用户的身份信息，本质上是要做两件事情。第一是保存用户的身份信息，第二是验证用户的身份信息。如果利用其它手段实现这两个目标，那么就可以不用session，或者说我们使用的是广义上的session了。</p>
<p>具体实现的思路如下图，我们在服务端增加两段程序：</p>
<p>第一是创建令牌的程序，就是在用户初次访问服务器时，给它创建一个唯一的身份标识，并且使用cookie封装这个标识再发送给客户端。那么当客户端下次再访问服务器时，就会自动携带这个身份标识了，这和SESSIONID的道理是一样的，只是改由我们自己来实现了。另外，在返回令牌之前，我们需要将它存储起来，以便于后续的验证。而这个令牌是不能保存在服务器本地的，因为其他服务器无法访问它。因此，我们可以将其存储在服务器之外的一个地方，那么Redis便是一个理想的场所。</p>
<p>第二是验证令牌的程序，就是在用户再次访问服务器时，我们获取到了它之前的身份标识，那么我们就要验证一下这个标识是否存在了。验证的过程很简单，我们从Redis中尝试获取一下就可以知道结果。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/session-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h2 id="1-25-如何利用Redis实现一个分布式锁？">1.25 如何利用Redis实现一个分布式锁？</h2>
<p><strong>参考答案</strong></p>
<p>何时需要分布式锁？</p>
<p>在分布式的环境下，当多个server并发修改同一个资源时，为了避免竞争就需要使用分布式锁。那为什么不能使用Java自带的锁呢？因为Java中的锁是面向多线程设计的，它只局限于当前的JRE环境。而多个server实际上是多进程，是不同的JRE环境，所以Java自带的锁机制在这个场景下是无效的。</p>
<h3 id="如何实现分布式锁？">如何实现分布式锁？</h3>
<p>**采用Redis实现分布式锁，就是在Redis里存一份代表锁的数据，通常用字符串即可。**实现分布式锁的思路，以及优化的过程如下：</p>
<ol>
<li>
<p>加锁：</p>
<p>第一版，这种方式的缺点是容易产生死锁，因为客户端有可能忘记解锁，或者解锁失败。</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">setnx key value<br></code></pre></td></tr></table></figure>
<p>第二版，给锁增加了过期时间，避免出现死锁。但这两个命令不是原子的，第二步可能会失败，依然无法避免死锁问题。</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">setnx key valueexpire key seconds<br></code></pre></td></tr></table></figure>
<p>第三版，通过“set…nx…”命令，将加锁、过期命令编排到一起，它们是原子操作了，可以避免死锁。</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">set key value nx ex seconds <br></code></pre></td></tr></table></figure>
</li>
<li>
<p>解锁：</p>
<p>解锁就是删除代表锁的那份数据。</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">del key<br></code></pre></td></tr></table></figure>
</li>
<li>
<p>问题：</p>
<p>看起来已经很完美了，但实际上还有隐患，如下图。进程A在任务没有执行完毕时，锁已经到期被释放了。等进程A的任务执行结束后，它依然会尝试释放锁，因为它的代码逻辑就是任务结束后释放锁。但是，它的锁早已自动释放过了，它此时释放的可能是其他线程的锁。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-2-163591411739751.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
</li>
</ol>
<p>想要解决这个问题，我们需要解决两件事情：</p>
<ol>
<li>在加锁时就要给锁设置一个标识，进程要记住这个标识。当进程解锁的时候，要进行判断，是自己持有的锁才能释放，否则不能释放。可以为key赋一个随机值，来充当进程的标识。</li>
<li>解锁时要先判断、再释放，这两步需要保证原子性，否则第二步失败的话，就会出现死锁。而获取和删除命令不是原子的，这就需要采用Lua脚本，通过Lua脚本将两个命令编排在一起，而整个Lua脚本的执行是原子的。</li>
</ol>
<p>按照以上思路，优化后的命令如下：</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta">#</span><span class="bash"> 加锁<span class="hljs-built_in">set</span> key random-value nx ex seconds <span class="hljs-comment"># 解锁if redis.call(&quot;get&quot;,KEYS[1]) == ARGV[1] then    return redis.call(&quot;del&quot;,KEYS[1])else    return 0end</span></span><br></code></pre></td></tr></table></figure>
<h3 id="基于RedLock算法的分布式锁：">基于RedLock算法的分布式锁：</h3>
<p>上述分布式锁的实现方案，是建立在单个主节点之上的。它的潜在问题如下图所示，如果进程A在主节点上加锁成功，然后这个主节点宕机了，则从节点将会晋升为主节点。若此时进程B在新的主节点上加锁成果，之后原主节点重启，成为了从节点，系统中将同时出现两把锁，这是违背锁的唯一性原则的。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-lock-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>总之，就是在单个主节点的架构上实现分布式锁，是无法保证高可用的。若要保证分布式锁的高可用，则可以采用多个节点的实现方案。这种方案有很多，而Redis的官方给出的建议是采用RedLock算法的实现方案。该算法基于多个Redis节点，它的基本逻辑如下：</p>
<ul>
<li>这些节点相互独立，不存在主从复制或者集群协调机制；</li>
<li>加锁：以相同的KEY向N个实例加锁，只要超过一半节点成功，则认定加锁成功；</li>
<li>解锁：向所有的实例发送DEL命令，进行解锁；</li>
</ul>
<p>RedLock算法的示意图如下，我们可以自己实现该算法，也可以直接使用Redisson框架。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-lock-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h2 id="1-26-说一说你对布隆过滤器的理解">1.26 说一说你对布隆过滤器的理解</h2>
<p><strong>参考答案</strong></p>
<p>布隆过滤器可以用很低的代价，估算出数据是否真实存在。<strong>例如：给用户推荐新闻时，要去掉重复的新闻，就可以利用布隆过滤器，判断该新闻是否已经推荐过。</strong></p>
<p>布隆过滤器的核心包括两部分：</p>
<ol>
<li><strong>一个大型的位数组</strong>；</li>
<li><strong>若干个不一样的哈希函数</strong>，每个哈希函数都能将哈希值算的比较均匀。</li>
</ol>
<p>布隆过滤器的工作原理：</p>
<ol>
<li>添加key时，每个哈希函数都利用这个key计算出一个哈希值，再根据哈希值计算一个位置，并将位数组中这个位置的值设置为1。</li>
<li>询问key时，每个哈希函数都利用这个key计算出一个哈希值，再根据哈希值计算一个位置。然后对比这些哈希函数在位数组中对应位置的数值：
<ul>
<li>如果这几个位置中，有一个位置的值是0，就说明这个布隆过滤器中，不存在这个key。</li>
<li>如果这几个位置中，所有位置的值都是1，就说明这个布隆过滤器中，极有可能存在这个key。之所以不是百分之百确定，是因为也可能是其他的key运算导致该位置为1。</li>
</ul>
</li>
</ol>
<h2 id="1-27-多台Redis抗高并发访问该怎么设计？（如果并发量超过30万，怎么设计Redis架构？）">1.27 多台Redis抗高并发访问该怎么设计？（如果并发量超过30万，怎么设计Redis架构？）</h2>
<p><strong>参考答案</strong></p>
<p>Redis Cluster是Redis的分布式解决方案，在3.0版本正式推出，有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时，可以采用Cluster架构方案达到负载均衡的目的。</p>
<p>Redis集群采用虚拟槽分区来实现数据分片，它把所有的键根据哈希函数映射到<code>0-16383</code>整数槽内，计算公式为<code>slot=CRC16(key)&amp;16383</code>，每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点：</p>
<ol>
<li>解耦数据和节点之间的关系，简化了节点扩容和收缩的难度；</li>
<li>节点自身维护槽的映射关系，不需要客户端或者代理服务维护槽分区元数据；</li>
<li>支持节点、槽、键之间的映射查询，用于数据路由，在线伸缩等场景。</li>
</ol>
<p>Redis集群中数据的分片逻辑如下图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/redis-8-163591418217855.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h1>5. 分布式</h1>
<h2 id="1-1-什么是CAP原则？">1.1 什么是CAP原则？</h2>
<p><strong>参考答案</strong></p>
<p>CAP定理又称CAP原则，指的是在一个分布式系统中，Consistency（一致性）、 Availability（可用性）、Partition tolerance（分区容错性），最多只能同时三个特性中的两个，三者不可兼得。</p>
<ul>
<li>
<p>Consistency (一致性)：</p>
<p>“all nodes see the same data at the same time”，即更新操作成功并返回客户端后，所有节点在同一时间的数据完全一致，这就是分布式的一致性。一致性的问题在并发系统中不可避免，对于客户端来说，一致性指的是并发访问时更新过的数据如何获取的问题。从服务端来看，则是更新如何复制分布到整个系统，以保证数据最终一致。</p>
</li>
<li>
<p>Availability (可用性)：</p>
<p>可用性指“Reads and writes always succeed”，即服务一直可用，而且是正常响应时间。好的可用性主要是指系统能够很好的为用户服务，不出现用户操作失败或者访问超时等用户体验不好的情况。</p>
</li>
<li>
<p>Partition Tolerance (分区容错性)：</p>
<p>即分布式系统在遇到某节点或网络分区故障的时候，仍然能够对外提供满足一致性和可用性的服务。分区容错性要求能够使应用虽然是一个分布式系统，而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了，其他剩下的机器还能够正常运转满足系统需求，对于用户而言并没有什么体验上的影响。</p>
</li>
</ul>
<h2 id="1-2-说一说你对高并发的理解">1.2 说一说你对高并发的理解</h2>
<p><strong>参考答案</strong></p>
<h3 id="1-如何理解高并发？">1. 如何理解高并发？</h3>
<p>高并发意味着大流量，需要运用技术手段抵抗流量的冲击，这些手段好比操作流量，能让流量更平稳地被系统所处理，带给用户更好的体验。我们常见的高并发场景有：淘宝的双11、春运时的抢票、微博大V的热点新闻等。除了这些典型事情，每秒几十万请求的秒杀系统、每天千万级的订单系统、每天亿级日活的信息流系统等，都可以归为高并发。很显然，上面谈到的高并发场景，并发量各不相同，那到底多大并发才算高并发呢？</p>
<ol>
<li>不能只看数字，要看具体的业务场景。不能说10W QPS的秒杀是高并发，而1W QPS的信息流就不是高并发。信息流场景涉及复杂的推荐模型和各种人工策略，它的业务逻辑可能比秒杀场景复杂10倍不止。因此，不在同一个维度，没有任何比较意义。</li>
<li>业务都是从0到1做起来的，并发量和QPS只是参考指标，最重要的是：在业务量逐渐变成原来的10倍、100倍的过程中，你是否用到了高并发的处理方法去演进你的系统，从架构设计、编码实现、甚至产品方案等维度去预防和解决高并发引起的问题？而不是一味的升级硬件、加机器做水平扩展。</li>
</ol>
<p>此外，各个高并发场景的业务特点完全不同：有读多写少的信息流场景、有读多写多的交易场景，那是否有通用的技术方案解决不同场景的高并发问题呢？我觉得大的思路可以借鉴，别人的方案也可以参考，但是真正落地过程中，细节上还会有无数的坑。另外，由于软硬件环境、技术栈、以及产品逻辑都没法做到完全一致，这些都会导致同样的业务场景，就算用相同的技术方案也会面临不同的问题，这些坑还得一个个趟。</p>
<h3 id="2-高并发系统设计的目标是什么？">\2. 高并发系统设计的目标是什么？</h3>
<p>先搞清楚高并发系统设计的目标，在此基础上再讨论设计方案和实践经验才有意义和针对性。</p>
<h4 id="2-1-宏观目标">2.1 宏观目标</h4>
<p>高并发绝不意味着只追求高性能，这是很多人片面的理解。从宏观角度看，高并发系统设计的目标有三个：高性能、高可用，以及高可扩展。</p>
<ol>
<li>高性能：性能体现了系统的并行处理能力，在有限的硬件投入下，提高性能意味着节省成本。同时，性能也反映了用户体验，响应时间分别是100毫秒和1秒，给用户的感受是完全不同的。</li>
<li>高可用：表示系统可以正常服务的时间。一个全年不停机、无故障；另一个隔三差五出线上事故、宕机，用户肯定选择前者。另外，如果系统只能做到90%可用，也会大大拖累业务。</li>
<li>高扩展：表示系统的扩展能力，流量高峰时能否在短时间内完成扩容，更平稳地承接峰值流量，比如双11活动、明星离婚等热点事件。</li>
</ol>
<p>这3个目标是需要通盘考虑的，因为它们互相关联、甚至也会相互影响。比如说：考虑系统的扩展能力，你会将服务设计成无状态的，这种集群设计保证了高扩展性，其实也间接提升了系统的性能和可用性。再比如说：为了保证可用性，通常会对服务接口进行超时设置，以防大量线程阻塞在慢请求上造成系统雪崩，那超时时间设置成多少合理呢？一般，我们会参考依赖服务的性能表现进行设置。</p>
<h4 id="2-2-微观目标">2.2 微观目标</h4>
<p>再从微观角度来看，高性能、高可用和高扩展又有哪些具体的指标来衡量？为什么会选择这些指标呢？</p>
<h5 id="2-2-1-性能指标">2.2.1 性能指标</h5>
<p>通过性能指标可以度量目前存在的性能问题，同时作为性能优化的评估依据。一般来说，会采用一段时间内的接口响应时间作为指标。</p>
<ol>
<li>
<p>平均响应时间：最常用，但是缺陷很明显，对于慢请求不敏感。比如1万次请求，其中9900次是1ms，100次是100ms，则平均响应时间为1.99ms，虽然平均耗时仅增加了0.99ms，但是1%请求的响应时间已经增加了100倍。</p>
</li>
<li>
<p>TP90、TP99等分位值：将响应时间按照从小到大排序，TP90表示排在第90分位的响应时间， 分位值越大，对慢请求越敏感。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/cap-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
</li>
<li>
<p>吞吐量：和响应时间呈反比，比如响应时间是1ms，则吞吐量为每秒1000次。</p>
</li>
</ol>
<p>通常，设定性能目标时会兼顾吞吐量和响应时间，比如这样表述：在每秒1万次请求下，AVG控制在50ms以下，TP99控制在100ms以下。对于高并发系统，AVG和TP分位值必须同时要考虑。另外，从用户体验角度来看，200毫秒被认为是第一个分界点，用户感觉不到延迟，1秒是第二个分界点，用户能感受到延迟，但是可以接受。因此，对于一个健康的高并发系统，TP99应该控制在200毫秒以内，TP999或者TP9999应该控制在1秒以内。</p>
<h5 id="2-2-2-可用性指标">2.2.2 可用性指标</h5>
<p>高可用性是指系统具有较高的无故障运行能力，可用性 = 平均故障时间 / 系统总运行时间，一般使用几个9来描述系统的可用性。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/cap-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>对于高并发系统来说，最基本的要求是：保证3个9或者4个9。原因很简单，如果你只能做到2个9，意味着有1%的故障时间，像一些大公司每年动辄千亿以上的GMV或者收入，1%就是10亿级别的业务影响。</p>
<h5 id="2-2-3-可扩展性指标">2.2.3 可扩展性指标</h5>
<p>面对突发流量，不可能临时改造架构，最快的方式就是增加机器来线性提高系统的处理能力。</p>
<p>对于业务集群或者基础组件来说，扩展性 = 性能提升比例 / 机器增加比例，理想的扩展能力是：资源增加几倍，性能提升几倍。通常来说，扩展能力要维持在70%以上。</p>
<p>但是从高并发系统的整体架构角度来看，扩展的目标不仅仅是把服务设计成无状态就行了，因为当流量增加10倍，业务服务可以快速扩容10倍，但是数据库可能就成为了新的瓶颈。</p>
<p>像MySQL这种有状态的存储服务通常是扩展的技术难点，如果架构上没提前做好规划（垂直和水平拆分），就会涉及到大量数据的迁移。</p>
<p>因此，高扩展性需要考虑：服务集群、数据库、缓存和消息队列等中间件、负载均衡、带宽、依赖的第三方等，当并发达到某一个量级后，上述每个因素都可能成为扩展的瓶颈点。</p>
<h3 id="3-高并发的实践方案有哪些？">\3. 高并发的实践方案有哪些？</h3>
<p>了解了高并发设计的3大目标后，再系统性总结下高并发的设计方案，会从以下两部分展开：先总结下通用的设计方法，然后再围绕高性能、高可用、高扩展分别给出具体的实践方案。</p>
<h4 id="3-1-通用的设计方法">3.1 通用的设计方法</h4>
<p>通用的设计方法主要是从「纵向」和「横向」两个维度出发，俗称高并发处理的两板斧：纵向扩展和横向扩展。</p>
<h5 id="3-1-1-纵向扩展（scale-up）">3.1.1 纵向扩展（scale-up）</h5>
<p>它的目标是提升单机的处理能力，方案又包括：</p>
<ol>
<li>提升单机的硬件性能：通过增加内存、CPU核数、存储容量、或者将磁盘升级成SSD等堆硬件的方式来提升。</li>
<li>提升单机的软件性能：使用缓存减少IO次数，使用并发或者异步的方式增加吞吐量。</li>
</ol>
<h5 id="3-1-2-横向扩展（scale-out）">3.1.2 横向扩展（scale-out）</h5>
<p>因为单机性能总会存在极限，所以最终还需要引入横向扩展，通过集群部署以进一步提高并发处理能力，又包括以下2个方向：</p>
<ol>
<li>
<p>做好分层架构：这是横向扩展的提前，因为高并发系统往往业务复杂，通过分层处理可以简化复杂问题，更容易做到横向扩展。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/cap-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>上面这种图是互联网最常见的分层架构，当然真实的高并发系统架构会在此基础上进一步完善。比如会做动静分离并引入CDN，反向代理层可以是LVS+Nginx，Web层可以是统一的API网关，业务服务层可进一步按垂直业务做微服务化，存储层可以是各种异构数据库。</p>
</li>
<li>
<p>各层进行水平扩展：无状态水平扩容，有状态做分片路由。业务集群通常能设计成无状态的，而数据库和缓存往往是有状态的，因此需要设计分区键做好存储分片，当然也可以通过主从同步、读写分离的方案提升读性能。</p>
</li>
</ol>
<h4 id="3-2-具体的实践方案">3.2 具体的实践方案</h4>
<p>下面再结合我的个人经验，针对高性能、高可用、高扩展3个方面，总结下可落地的实践方案。</p>
<h5 id="3-2-1-高性能的实践方案">3.2.1 高性能的实践方案</h5>
<ol>
<li>集群部署，通过负载均衡减轻单机压力。</li>
<li>多级缓存，包括静态数据使用CDN、本地缓存、分布式缓存等，以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。</li>
<li>分库分表和索引优化，以及借助搜索引擎解决复杂查询问题。</li>
<li>考虑NoSQL数据库的使用，比如HBase、TiDB等，但是团队必须熟悉这些组件，且有较强的运维能力。</li>
<li>异步化，将次要流程通过多线程、MQ、甚至延时任务进行异步处理。</li>
<li>限流，需要先考虑业务是否允许限流（比如秒杀场景是允许的），包括前端限流、Nginx接入层的限流、服务端的限流。</li>
<li>对流量进行削峰填谷，通过MQ承接流量。</li>
<li>并发处理，通过多线程将串行逻辑并行化。</li>
<li>预计算，比如抢红包场景，可以提前计算好红包金额缓存起来，发红包时直接使用即可。</li>
<li>缓存预热，通过异步任务提前预热数据到本地缓存或者分布式缓存中。</li>
<li>减少IO次数，比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。</li>
<li>减少IO时的数据包大小，包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。</li>
<li>程序逻辑优化，比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化，或者采用更高效的算法。</li>
<li>各种池化技术的使用和池大小的设置，包括HTTP请求池、线程池（考虑CPU密集型还是IO密集型设置核心参数）、数据库和Redis连接池等。</li>
<li>JVM优化，包括新生代和老年代的大小、GC算法的选择等，尽可能减少GC频率和耗时。</li>
<li>锁选择，读多写少的场景用乐观锁，或者考虑通过分段锁的方式减少锁冲突。</li>
</ol>
<p>上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点，需要有配套的监控系统实时了解当前的性能表现，并支撑你进行性能瓶颈分析，然后再遵循二八原则，抓主要矛盾进行优化。</p>
<h5 id="3-2-2-高可用的实践方案">3.2.2 高可用的实践方案</h5>
<ol>
<li>对等节点的故障转移，Nginx和服务治理框架均支持一个节点失败后访问另一个节点。</li>
<li>非对等节点的故障转移，通过心跳检测并实施主备切换（比如redis的哨兵模式或者集群模式、MySQL的主从切换等）。</li>
<li>接口层面的超时设置、重试策略和幂等设计。</li>
<li>降级处理：保证核心服务，牺牲非核心服务，必要时进行熔断；或者核心链路出问题时，有备选链路。</li>
<li>限流处理：对超过系统处理能力的请求直接拒绝或者返回错误码。</li>
<li>MQ场景的消息可靠性保证，包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。</li>
<li>灰度发布，能支持按机器维度进行小流量部署，观察系统日志和业务指标，等运行平稳后再推全量。</li>
<li>监控报警：全方位的监控体系，包括最基础的CPU、内存、磁盘、网络的监控，以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。</li>
<li>灾备演练：类似当前的“混沌工程”，对系统进行一些破坏性手段，观察局部故障是否会引起可用性问题。</li>
</ol>
<p>高可用的方案主要从冗余、取舍、系统运维3个方向考虑，同时需要有配套的值班机制和故障处理流程，当出现线上问题时，可及时跟进处理。</p>
<h5 id="3-2-3-高扩展的实践方案">3.2.3 高扩展的实践方案</h5>
<ol>
<li>合理的分层架构：比如上面谈到的互联网最常见的分层架构，另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层（但是需要评估性能，会存在网络多一跳的情况）。</li>
<li>存储层的拆分：按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分（分库分表）。</li>
<li>业务层的拆分：最常见的是按照业务维度拆（比如电商场景的商品服务、订单服务等），也可以按照核心接口和非核心接口拆，还可以按照请求源拆（比如To C和To B，APP和H5）。</li>
</ol>
<h2 id="1-3-如何实现分布式存储？">1.3 如何实现分布式存储？</h2>
<p><strong>参考答案</strong></p>
<p>分布式存储是一个大的概念，其包含的种类繁多，除了传统意义上的分布式文件系统、分布式块存储和分布式对象存储外，还包括分布式数据库和分布式缓存等。下面我们探讨一下分布式文件系统等传统意义上的存储架构，实现这种存储架构主要有三种通用的形式，其它存储架构也基本上基于上述架构，并没有太大的变化。</p>
<p>中间控制节点架构（HDFS）</p>
<p>分布式存储最早是由谷歌提出的，其目的是通过廉价的服务器来提供使用与大规模，高并发场景下的Web访问问题。下图是谷歌分布式存储（HDFS）的简化的模型。在该系统的整个架构中将服务器分为两种类型，一种名为namenode，这种类型的节点负责管理管理数据（元数据），另外一种名为datanode，这种类型的服务器负责实际数据的管理。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>上图分布式存储中，如果客户端需要从某个文件读取数据，首先从namenode获取该文件的位置（具体在哪个datanode），然后从该位置获取具体的数据。在该架构中namenode通常是主备部署，而datanode则是由大量节点构成一个集群。由于元数据的访问频度和访问量相对数据都要小很多，因此namenode通常不会成为性能瓶颈，而datanode集群可以分散客户端的请求。因此，通过这种分布式存储架构可以通过横向扩展datanode的数量来增加承载能力，也即实现了动态横向扩展的能力。</p>
<p>完全无中心架构—计算模式（Ceph）</p>
<p>下图是Ceph存储系统的架构，在该架构中与HDFS不同的地方在于该架构中没有中心节点。客户端是通过一个设备映射关系计算出来其写入数据的位置，这样客户端可以直接与存储节点通信，从而避免中心节点的性能瓶颈。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>在Ceph存储系统架构中核心组件有Mon服务、OSD服务和MDS服务等。对于块存储类型只需要Mon服务、OSD服务和客户端的软件即可。其中Mon服务用于维护存储系统的硬件逻辑关系，主要是服务器和硬盘等在线信息。Mon服务通过集群的方式保证其服务的可用性。OSD服务用于实现对磁盘的管理，实现真正的数据读写，通常一个磁盘对应一个OSD服务。<br>
客户端访问存储的大致流程是，客户端在启动后会首先从Mon服务拉取存储资源布局信息，然后根据该布局信息和写入数据的名称等信息计算出期望数据的位置（包含具体的物理服务器信息和磁盘信息），然后该位置信息直接通信，读取或者写入数据。</p>
<p>完全无中心架构—一致性哈希（Swift）</p>
<p>与Ceph的通过计算方式获得数据位置的方式不同，另外一种方式是通过一致性哈希的方式获得数据位置。一致性哈希的方式就是将设备做成一个哈希环，然后根据数据名称计算出的哈希值映射到哈希环的某个位置，从而实现数据的定位。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>上图是一致性哈希的基本原理，为了绘制简单，本文以一个服务器上的一个磁盘为例进行介绍。为了保证数据分配的均匀性及出现设备故障时数据迁移的均匀性，一致性哈希将磁盘划分为比较多的虚拟分区，每个虚拟分区是哈希环上的一个节点。整个环是一个从0到32位最大值的一个区间，并且首尾相接。当计算出数据（或者数据名称）的哈希值后，必然落到哈希环的某个区间，然后以顺时针，必然能够找到一个节点。那么，这个节点就是存储数据的位置。<br>
Swift存储的整个数据定位算法就是基于上述一致性哈希实现的。在Swift对象存储中，通过账户名/容器名/对象名三个名称组成一个位置的标识，通过该唯一标识可以计算出一个整型数来。而在存储设备方面，Swift构建一个虚拟分区表，表的大小在创建集群是确定（通常为几十万），这个表其实就是一个数组。这样，根据上面计算的整数值，以及这个数组，通过一致性哈希算法就可以确定该整数在数组的位置。而数组中的每项内容是数据3个副本（也可以是其它副本数量）的设备信息（包含服务器和磁盘等信息）。也就是经过上述计算，可以确定一个数据存储的具体位置。这样，Swift就可以将请求重新定向到该设备进行处理。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-4.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>上述计算过程是在一个名为Proxy的服务中进行的，该服务可以集群化部署。因此可以分摊请求的负载，不会成为性能瓶颈。</p>
<h2 id="1-4-说一说你对分布式事务的了解">1.4 说一说你对分布式事务的了解</h2>
<p><strong>参考答案</strong></p>
<p>分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说，就是一次大的操作由不同的小操作组成，这些小的操作分布在不同的服务器上，且属于不同的应用，分布式事务需要保证这些小操作要么全部成功，要么全部失败。本质上来说，分布式事务就是为了保证不同数据库的数据一致性。</p>
<p>要实现分布式事务，有如下几种常见的解决方案：</p>
<p>2PC</p>
<p>说到2PC就不得不聊数据库分布式事务中的 XA Transactions。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-tx-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>如上图，在XA协议中分为两阶段：</p>
<p>第一阶段：事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作，并反映是否可以提交。</p>
<p>第二阶段：事务协调器要求每个数据库提交数据，或者回滚数据。</p>
<p>优点：</p>
<ul>
<li>尽量保证了数据的强一致，实现成本较低，在各大主流数据库都有自己实现，对于MySQL是从5.5开始支持。</li>
</ul>
<p>缺点：</p>
<ul>
<li>单点问题:事务管理器在整个流程中扮演的角色很关键，如果其宕机，比如在第一阶段已经完成，在第二阶段正准备提交的时候事务管理器宕机，资源管理器就会一直阻塞，导致数据库无法使用。</li>
<li>同步阻塞:在准备就绪之后，资源管理器中的资源一直处于阻塞，直到提交完成，释放资源。</li>
<li>数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计，但仍然存在数据不一致性的可能，比如在第二阶段中，假设协调者发出了事务commit的通知，但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作，其余的参与者则因为没有收到通知一直处于阻塞状态，这时候就产生了数据的不一致性。</li>
</ul>
<p>总的来说，XA协议比较简单，成本较低，但是其单点问题，以及不能支持高并发依然是其最大的弱点。</p>
<h2 id="1-5-分布式系统如何保证最终一致性？">1.5 分布式系统如何保证最终一致性？</h2>
<p><strong>参考答案</strong></p>
<p>国际开放标准组织Open Group定义了DTS（分布式事务处理模型），模型中包含4种角色：应用程序、事务管理器、资源管理器和通信资源管理器。事务管理器是统管全局的管理者，资源管理器和通信资源管理器是事务的参与者。</p>
<p>JEE（Java企业版）规范也包含此分布式事务处理模型的规范，并在所有AppServer中进行实现。在JEE规范中定义了TX协议和XA协议，TX协议定义应用程序与事务管理器之间的接口，XA协议则定义事务管理器与资源管理器之间的接口。在过去使用 AppServer如WebSphere、 WebLogic、JBoss等配置数据源时会看见类似XADatasource的数据源，这就是实现了分布式事务处理模型的关系型数据库的数据源。在企业级开发JEE中，关系型数据库、JMS服务扮演资源管理器的角色，而EJB容器扮演事务管理器的角色。</p>
<p>下面我们介绍两阶段提交协议、三阶段提交协议及阿里巴巴提出的 TCC，它们都是根据DTS这一思想演变而来的。</p>
<p>两阶段提交协议</p>
<p>两阶段提交协议把分布式事务分为两个阶段，一个是准备阶段，另一个是提交阶段。准备阶段和提交阶段都是由事务管理器发起的，为了接下来讲解方便，我们将事务管理器称为协调者，将资源管理器称为参与者。</p>
<p>两阶段提交协议的流程如下所述。</p>
<ul>
<li>
<p>准备阶段：协调者向参与者发起指令，参与者评估自己的状态，如果参与者评估指令可以完成，则会写redo或者undo日志（Write-Ahead Log的一种），然后锁定资源，执行操作，但是并不提交。</p>
</li>
<li>
<p>提交阶段：如果每个参与者明确返回准备成功，也就是预留资源和执行操作成功，则协调者向参与者发起提交指令，参与者提交资源变更的事务，释放锁定的资源；如果任何一个参与者明确返回准备失败，也就是预留资源或者执行操作失败，则协调者向参与者发起中止指令，参与者取消已经变更的事务，执行undo日志，释放锁定的资源。两阶段提交协议的成功场景如下图所示。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-yzx-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>我们看到两阶段提交协议在准备阶段锁定资源，这是一个重量级的操作，能保证强一致性，但是实现起来复杂、成本较高、不够灵活，更重要的是它有如下致命的问题。</p>
</li>
<li>
<p>阻塞：从上面的描述来看，对于任何一次指令都必须收到明确的响应，才会继续进行下一步，否则处于阻塞状态，占用的资源被一直锁定，不会被释放。</p>
</li>
<li>
<p>单点故障：如果协调者宕机，参与者没有协调者指挥，则会一直阻塞，尽管可以通过选举新的协调者替代原有协调者，但是如果协调者在发送一个提交指令后宕机，而提交指令仅仅被一个参与者接收，并且参与者接收后也宕机，则新上任的协调者无法处理这种情况。</p>
</li>
<li>
<p>脑裂：协调者发送提交指令，有的参与者接收到并执行了事务，有的参与者没有接收到事务就没有执行事务，多个参与者之间是不一致的。</p>
</li>
</ul>
<p>上面的所有问题虽然很少发生，但都需要人工干预处理，没有自动化的解决方案，因此两阶段提交协议在正常情况下能保证系统的强一致性，但是在出现异常的情况下，当前处理的操作处于错误状态，需要管理员人工干预解决，因此可用性不够好，这也符合CAP协议的一致性和可用性不能兼得的原理。</p>
<p>三阶段提交协议</p>
<p>三阶段提交协议是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题，并且把两个阶段增加为以下三个阶段。</p>
<ul>
<li>询问阶段：协调者询问参与者是否可以完成指令，协调者只需要回答是或不是，而不需要做真正的操作，这个阶段超时会导致中止。</li>
<li>准备阶段：如果在询问阶段所有参与者都返回可以执行操作，则协调者向参与者发送预执行请求，然后参与者写redo和undo日志，执行操作但是不提交操作；如果在询问阶段任意参与者返回不能执行操作的结果，则协调者向参与者发送中止请求，这里的逻辑与两阶段提交协议的准备阶段是相似的。</li>
<li>提交阶段：如果每个参与者在准备阶段返回准备成功，也就是说预留资源和执行操作成功，则协调者向参与者发起提交指令，参与者提交资源变更的事务，释放锁定的资源；如果任何参与者返回准备失败，也就是说预留资源或者执行操作失败，则协调者向参与者发起中止指令，参与者取消已经变更的事务，执行 undo 日志，释放锁定的资源，这里的逻辑与两阶段提交协议的提交阶段一致。</li>
</ul>
<p>三阶段提交协议的成功场景示意图如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-yzx-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>三阶段提交协议与两阶段提交协议主要有以下两个不同点：</p>
<ul>
<li>增加了一个询问阶段，询问阶段可以确保尽可能早地发现无法执行操作而需要中止的行为，但是它并不能发现所有这种行为，只会减少这种情况的发生。</li>
<li>在准备阶段以后，协调者和参与者执行的任务中都增加了超时，一旦超时，则协调者和参与者都会继续提交事务，默认为成功，这也是根据概率统计超时后默认为成功的正确性最大。</li>
</ul>
<p>三阶段提交协议与两阶段提交协议相比，具有如上优点，但是一旦发生超时，系统仍然会发生不一致，只不过这种情况很少见，好处是至少不会阻塞和永远锁定资源。</p>
<p>TCC</p>
<p>签名讲解了两阶段提交协议和三阶段提交协议，实际上它们能解决常见的分布式事务的问题，但是遇到极端情况时，系统会产生阻塞或者不一致的问题，需要运营或者技术人员解决。两阶段及三阶段方案中都包含多个参与者、多个阶段实现一个事务，实现复杂，性能也是一个很大的问题，因此，在互联网的高并发系统中，鲜有使用两阶段提交和三阶段提交协议的场景。</p>
<p>后来有人提出了TCC协议，TCC协议将一个任务拆分成Try、Confirm、Cancel三个步骤，正常的流程会先执行Try，如果执行没有问题，则再执行Confirm，如果执行过程中出了问题，则执行操作的逆操作Cancel。从正常的流程上讲，这仍然是一个两阶段提交协议，但是在执行出现问题时有一定的自我修复能力，如果任何参与者出现了问题，则协调者通过执行操作的逆操作来Cancel之前的操作，达到最终的一致状态。</p>
<p>可以看出，从时序上来说，如果遇到极端情况，则TCC会有很多问题，例如，如果在取消时一些参与者收到指令，而另一些参与者没有收到指令，则整个系统仍然是不一致的。对于这种复杂的情况，系统首先会通过补偿的方式尝试自动修复，如果系统无法修复，则必须由人工参与解决。</p>
<p>从TCC的逻辑上看，可以说TCC是简化版的三阶段提交协议，解决了两阶段提交协议的阻塞问题，但是没有解决极端情况下会出现不一致和脑裂的问题。然而，TCC通过自动化补偿手段，将需要人工处理的不一致情况降到最少，也是一种非常有用的解决方案。某著名的互联网公司在内部的一些中间件上实现了TCC模式。</p>
<p>我们给出一个使用TCC的实际案例，在秒杀的场景中，用户发起下订单请求，应用层先查询库存，确认商品库存还有余量，则锁定库存，此时订单状态为待支付，然后指引用户去支付，由于某种原因用户支付失败或者支付超时，则系统会自动将锁定的库存解锁以供其他用户秒杀。</p>
<p>TCC协议的使用场景如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-yzx-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>在大规模、高并发服务化系统中，一个功能被拆分成多个具有单一功能的子功能，一个流程会有多个系统的多个单一功能的服务组合实现，如果使用两阶段提交协议和三阶段提交协议，则确实能解决系统间的一致性问题。除了这两个协议的自身问题，其实现也比较复杂、成本比较高，最重要的是性能不好，相比来看，TCC协议更简单且更容易实现，但是TCC协议由于每个事务都需要执行Try，再执行Confirm，略显臃肿，因此，现实系统的底线是仅仅需要达到最终一致性，而不需要实现专业的、复杂的一致性协议。实现最终一致性有一些非常有效、简单的模式，下面就介绍这些模式及其应用场景。</p>
<p>查询模式</p>
<p>任何服务操作都需要提供一个查询接口，用来向外部输出操作执行的状态。服务操作的使用方可以通过查询接口得知服务操作执行的状态，然后根据不同的状态来做不同的处理操作。</p>
<p>为了能够实现查询，每个服务操作都需要有唯一的流水号标识，也可使用此次服务操作对应的资源ID来标识，例如：请求流水号、订单号等。首先，单笔查询操作是必须提供的，也鼓励使用单笔订单查询，这是因为每次调用需要占用的负载是可控的。批量查询则根据需要来提供，如果使用了批量查询，则需要有合理的分页机制，并且必须限制分页的大小，以及对批量查询的吞吐量有容量评估、熔断、隔离和限流等措施。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-yzx-4.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>补偿模式</p>
<p>有了上面的查询模式，在任何情况下，我们都能得知具体的操作所处的状态，如果整个操作都处于不正常的状态，则我们需要修正操作中有问题的子操作，这可能需要重新执行未完成的子操作，后者取消已经完成的子操作，通过修复使整个分布式系统达到一致。为了让系统最终达到一致状态而做的努力都叫作补偿。</p>
<p>对于服务化系统中同步调用的操作，若业务操作发起方还没有收到业务操作执行方的明确返回或者调用超时，业务发起方需要及时地调用业务执行方来获得操作执行的状态，这里使用在前面学习的查询模式。在获得业务操作执行方的状态后，如果业务执行方已经完成预设工作，则业务发起方向业务的使用方返回成功；如果业务操作执行方的状态为失败或者未知，则会立即告诉业务使用方失败，也叫作快速失败策略，然后调用业务操作的逆向操作，保证操作不被执行或者回滚已经执行的操作，让业务使用方、业务操作发起方和业务操作执行方最终达到一致状态。</p>
<p>补偿模式如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-yzx-5.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>异步确保模式</p>
<p>异步确保模式是补偿模式的一个典型案例，经常应用到使用方对响应时间要求不太高的场景中，通常把这类操作从主流程中摘除，通过异步的方式进行处理，处理后把结果通过通知系统通知给使用方。这个方案的最大好处是能够对高并发流量进行消峰，例如：电商系统中的物流、配送，以及支付系统中的计费、入账等。</p>
<p>在实践中将要执行的异步操作封装后持久入库，然后通过定时捞取未完成的任务进行补偿操作来实现异步确保模式，只要定时系统足够健壮，则任何任务最终都会被成功执行。</p>
<p>异步确保模式如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dist-yzx-6.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>定期校对模式</p>
<p>系统在没有达到一致之前，系统间的状态是不一致的，甚至是混乱的，需要通过补偿操作来达到最终一致性的目的，但是如何来发现需要补偿的操作呢？</p>
<p>在操作主流程中的系统间执行校对操作，可以在事后异步地批量校对操作的状态，如果发现不一致的操作，则进行补偿，补偿操作与补偿模式中的补偿操作是一致的。</p>
<p>另外，实现定期校对的一个关键就是分布式系统中需要有一个自始至终唯一的ID，生成全局唯一ID有以下两种方法：</p>
<ul>
<li>持久型：使用数据库表自增字段或者Sequence生成，为了提高效率，每个应用节点可以缓存一个批次的ID，如果机器重启则可能会损失一部分ID，但是这并不会产生任何问题。</li>
<li>时间型：一般由机器号、业务号、时间、单节点内自增ID组成，由于时间一般精确到秒或者毫秒，因此不需要持久就能保证在分布式系统中全局唯一、粗略递增等。</li>
</ul>
<p>可靠消息模式</p>
<p>在分布式系统中，对于主流程中优先级比较低的操作，大多采用异步的方式执行，也就是前面提到的异步确保模型，为了让异步操作的调用方和被调用方充分解耦，也由于专业的消息队列本身具有可伸缩、可分片、可持久等功能，我们通常通过消息队列实现异步化。对于消息队列，我们需要建立特殊的设施来保证可靠的消息发送及处理机的幂等性。</p>
<p>缓存一致性模式</p>
<p>在大规模、高并发系统中的一个常见的核心需求就是亿级的读需求，显然，关系型数据库并不是解决高并发读需求的最佳方案，互联网的经典做法就是使用缓存来抗住读流量。</p>
<ul>
<li>如果性能要求不是非常高，则尽量使用分布式缓存，而不要使用本地缓存。</li>
<li>写缓存时数据一定要完整，如果缓存数据的一部分有效，另一部分无效，则宁可在需要时回源数据库，也不要把部分数据放入缓存中。</li>
<li>使用缓存牺牲了一致性，为了提高性能，数据库与缓存只需要保持弱一致性，而不需要保持强一致性，否则违背了使用缓存的初衷。</li>
<li>读的顺序是先读缓存，后读数据库，写的顺序要先写数据库，后写缓存。</li>
</ul>
<h2 id="1-6-谈谈你对分布式的单点问题的了解">1.6 谈谈你对分布式的单点问题的了解</h2>
<p><strong>参考答案</strong></p>
<p>在分布式系统中，单点问题是一个比较常见的问题，对于单点问题可以分为有状态服务的单点问题和无状态服务的单点问题。</p>
<p>无状态服务的单点问题</p>
<p>对于无状态的服务，单点问题的解决比较简单，因为服务是无状态的，所以服务节点很容易进行平行扩展。比如，在分布式系统中，为了降低各进程通信的网络结构的复杂度，我们会增加一个代理节点，专门做消息的转发，其他的业务进行直接和代理节点进行通信，类似一个星型的网络结构。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dandian-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dandian-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>参考上面两个图，图中proxy是一个消息转发代理，业务进程中的消息都会经过该代理，这也是比较场景的一个架构。在上图中，只有一个proxy，如果该节点挂了，那么所有的业务进程之间都无法进行通信。由于proxy是无状态的服务，所以很容易想到第二个图中的解决方案，增加一个proxy节点，两个proxy节点是对等的。增加新节点后，业务进程需要与两个Proxy之间增加一个心跳的机制，业务进程在发送消息的时候根据proxy的状态，选择一个可用的proxy进行消息的传递。从负载均衡的角度来看，如果两个proxy都是存活状态的话，业务进程应当随机选择一个proxy。</p>
<p>那么该解决方案中会存在什么问题呢？主要存在的问题是消息的顺序性问题。一般来说，业务的消息都是发送、应答，再发送、再应答这样的顺序进行的，在业务中可以保证消息的顺序性。但是，在实际的应用中，会出现这样一个情况：在业务进程1中，有个业务需要给业务进程3发送消息A和消息B，根据业务的特性，消息A必须要在消息B之前到达。如果业务进程1在发送消息A的时候选择了proxy1，在发送消息B的时候选择了proxy2，那么在分布式环境中，我们并不能确保先发送的消息A一定就能比后发送的消息B先到达业务进程3。那么怎么解决这个问题？其实方案也比较简单，对于这类对消息顺序有要求的业务，我们可以指定对应的proxy进行发送，比如消息A和消息B都是使用proxy1进行发送，这样就可以保证消息A比消息B先到达业务进程3。</p>
<p>整体来说，对于无状态的服务的单点问题的解决方案还是比较简单的，只要增加对应的服务节点即可。</p>
<p>有状态服务的单点问题</p>
<p>相对无状态服务的单点问题，有状态服务的单点问题就复杂多了。如果在架构中，有个节点是单点的，并且该节点是有状态的服务，那么首先要考虑的是该节点是否可以去状态，如果可以，则优先选择去除状态的方案（比如说把状态存储到后端的可靠DB中，可能存在性能的损耗），然后就退化成了一个无状态服务的单点问题了，这就可以参考上一方案了。<br>
但是，并不是所有的服务都是可以去状态的，比如说对于一些业务它只能在一个节点中进行处理，如果在不同的节点中处理的话可能会造成状态的不一致，这类型的业务是无法去除状态的。对于这种无法去除状态的单点的问题的解决方案也是有多种，但是越完善的方案实现起来就越复杂，不过整体的思路都是采用主备的方式。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dandian-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>第一个方案就是就是增加一个备用节点，备用节点和业务进程也可以进行通信，但是所有的业务消息都发往Master节点进行处理。Master节点和Slave节点之间采用ping的方式进行通信。Slave节点会定时发送ping包给Master节点，Master节点收到后会响应一个Ack包。当Slave节点发现Master节点没有响应的时候，就会认为Master节点挂了，然后把自己升级为Master节点，并且通知业务进程把消息转发给自己。该方案看起来也是挺完美的，好像不存在什么问题，Slave升级为Master后所有的业务消息都会发给它。但是，如果在Master内部有一些自己的业务逻辑，比如说随机生成一些业务数据，并且定时存档。那么当Master和Slave之间的网络出现问题的时候，Slave会认为Master挂了，就会升级为Master，同样会执行Master的相应的业务逻辑，同样也会生成一些业务数据回写到DB。但是，其实Master是没有挂的，它同样也在运行对应的业务逻辑（即使业务进程的消息没有发给旧的Master了），这样就会出现两个Master进行写同一份数据了，造成数据的混乱。所以说，该方案并不是一个很好的方案。</p>
<p>那怎么解决可能会出现多个Master的问题？换个角度看，该问题其实就是怎么去裁决哪个节点是Master的问题。</p>
<p>方案一：引入第三方的服务进行裁决。</p>
<p>我们可以引入ZooKeeper，由ZooKeeper进行裁决。同样，我们启动两个主节点，“节点A”和节点B。它们启动之后向ZooKeeper去注册一个节点，假设节点A注册的节点为master001，节点B注册的节点为master002，注册完成后进行选举，编号小的节点为真正的主节点。那么，通过这种方式就完成了对两个Master进程的调度。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dandian-4.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>方案二： 通过选举算法和租约的方式实现Master的选举。</p>
<p>对于方案一的缺点主要要多维护一套ZooKeeper的服务，如果原本业务上并没有部署该服务的话，要增加该服务的维护也是比较麻烦的事情。这个时候我们可以在业务进程中加入Master的选举方案。目前有比较成熟的选举算法，比如Paxos和Raft。然后再配合租约机制，就可以实现Master的选举，并且确保当前只有一个Master的方案。但是，这些选举算法理解起来并不是那么地容易，要实现一套完善的方案也是挺难的。所以不建议重复造轮子，业内有很多成熟的框架或者组件可以使用，比如微信的PhxPaxos。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/dandian-5.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>比如上图的方案中，三个节点其实都是对等的，通过选举算法确定一个Master。为了确保任何时候都只能存在一个Matster，需要加入租约的机制。一个节点成为Master后，Master和非Master节点都会进行计时，在超过租约时间后，三个节点后可以发起“我要成为Master”的请求，进行重新选举。由于三个节点都是对等的，任意一个都可以成为Master，也就是说租期过后，有可能会出现Master切换的情况，所以为了避免Master的频繁切换，Master节点需要比另外两个节点先发起自己要成为Master的请求（续租），告诉其他两个节点我要继续成为Master，然后另外两个节点收到请求后会进行应答，正常情况下另外两个节点会同意该请求。关键点就是，在租约过期之前，非Master节点不能发起“我要成为Master”的请求，这样就可以解决Master频繁切换的问题。</p>
<h2 id="1-7-HTTP和RPC有什么区别？">1.7 HTTP和RPC有什么区别？</h2>
<p><strong>参考答案</strong></p>
<p>传输协议</p>
<ul>
<li>RPC，可以基于TCP协议，也可以基于HTTP协议。</li>
<li>HTTP，基于HTTP协议。</li>
</ul>
<p>传输效率</p>
<ul>
<li>RPC，使用自定义的TCP协议，可以让请求报文体积更小，或者使用HTTP2协议，也可以很好的减少报文的体积，提高传输效率。</li>
<li>HTTP，如果是基于HTTP1.1的协议，请求中会包含很多无用的内容，如果是基于HTTP2.0，那么简单的封装一下是可以作为一个RPC来使用的，这时标准RPC框架更多的是服务治理。</li>
</ul>
<p>性能消耗</p>
<ul>
<li>RPC，可以基于thrift实现高效的二进制传输。</li>
<li>HTTP，大部分是通过json来实现的，字节大小和序列化耗时都比thrift要更消耗性能。</li>
</ul>
<p>负载均衡</p>
<ul>
<li>RPC，基本都自带了负载均衡策略。</li>
<li>HTTP，需要配置Nginx，HAProxy来实现。</li>
</ul>
<p>服务治理</p>
<ul>
<li>RPC，能做到自动通知，不影响上游。</li>
<li>HTTP，需要事先通知，修改Nginx/HAProxy配置。</li>
</ul>
<p>总之，RPC主要用于公司内部的服务调用，性能消耗低，传输效率高，服务治理方便。HTTP主要用于对外的异构环境，浏览器接口调用，APP接口调用，第三方接口调用等。</p>
<h1>6. 场景应用（秒杀、扫码登陆、单点登录、本地缓存）</h1>
<h2 id="1-2-秒杀系统相关问题">1.2 秒杀系统相关问题</h2>
<p><strong>参考答案</strong></p>
<p>秒杀应该考虑哪些问题？</p>
<ol>
<li>
<p>超卖问题</p>
<p>分析秒杀的业务场景,最重要的有一点就是超卖问题，假如备货只有100个，但是最终超卖了200，一般来讲秒杀系统的价格都比较低，如果超卖将严重影响公司的财产利益，因此首当其冲的就是解决商品的超卖问题。</p>
</li>
<li>
<p>高并发</p>
<p>秒杀具有时间短、并发量大的特点，秒杀持续时间只有几分钟，而一般公司都为了制造轰动效应，会以极低的价格来吸引用户，因此参与抢购的用户会非常的多。短时间内会有大量请求涌进来，后端如何防止并发过高造成缓存击穿或者失效，击垮数据库都是需要考虑的问题。</p>
</li>
<li>
<p>接口防刷</p>
<p>现在的秒杀大多都会出来针对秒杀对应的软件，这类软件会模拟不断向后台服务器发起请求，一秒几百次都是很常见的，如何防止这类软件的重复无效请求，防止不断发起的请求也是需要我们针对性考虑的。</p>
</li>
<li>
<p>秒杀URL</p>
<p>对于普通用户来讲，看到的只是一个比较简单的秒杀页面，在未达到规定时间，秒杀按钮是灰色的，一旦到达规定时间，灰色按钮变成可点击状态。这部分是针对小白用户的，如果是稍微有点电脑功底的用户，会通过F12看浏览器的network看到秒杀的url，通过特定软件去请求也可以实现秒杀。或者提前知道秒杀url的人，一请求就直接实现秒杀了。这个问题我们需要考虑解决。</p>
</li>
<li>
<p>数据库设计</p>
<p>秒杀有把我们服务器击垮的风险，如果让它与我们的其他业务使用在同一个数据库中，耦合在一起，就很有可能牵连和影响其他的业务。如何防止这类问题发生，就算秒杀发生了宕机、服务器卡死问题，也应该让他尽量不影响线上正常进行的业务。</p>
</li>
</ol>
<p>秒杀系统的设计方案</p>
<ol>
<li>
<p>秒杀系统的数据库设计</p>
<p>针对秒杀的数据库问题，应该单独设计一个秒杀数据库，防止因为秒杀活动的高并发访问拖垮整个网站。这里只需要两张表，一张是秒杀订单表，一张是秒杀货品表：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/miaosha-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/miaosha-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>其实应该还有几张表，商品表：可以关联goods_id查到具体的商品信息，商品图像、名称、平时价格、秒杀价格等，还有用户表：根据用户user_id可以查询到用户昵称、用户手机号，收货地址等其他额外信息，这个具体就不给出实例了。</p>
</li>
<li>
<p>秒杀URL的设计</p>
<p>为了避免有程序访问经验的人通过下单页面url直接访问后台接口来秒杀货品，我们需要将秒杀的url实现动态化，即使是开发整个系统的人都无法在秒杀开始前知道秒杀的url。具体的做法就是通过md5加密一串随机字符作为秒杀的url，然后前端访问后台获取具体的url，后台校验通过之后才可以继续秒杀。</p>
</li>
<li>
<p>秒杀页面静态化</p>
<p>将商品的描述、参数、成交记录、图像、评价等全部写入到一个静态页面，用户请求不需要通过访问后端服务器，不需要经过数据库，直接在前台客户端生成，这样可以最大可能的减少服务器的压力。具体的方法可以使用freemarker模板技术，建立网页模板，填充数据，然后渲染网页。</p>
</li>
<li>
<p>单体redis升级为集群redis</p>
<p>秒杀是一个读多写少的场景，使用redis做缓存再合适不过。不过考虑到缓存击穿问题，我们应该构建redis集群，或采用哨兵模式，可以提升redis的性能和可用性。</p>
</li>
<li>
<p>使用nginx</p>
<p>nginx是一个高性能web服务器，它的并发能力可以达到几万，而tomcat只有几百。通过nginx映射客户端请求，再分发到后台tomcat服务器集群中可以大大提升并发能力。</p>
</li>
<li>
<p>精简SQL</p>
<p>典型的一个场景是在进行扣减库存的时候，传统的做法是先查询库存，再去update。这样的话需要两个sql，而实际上一个sql我们就可以完成的。可以用这样的做法：<code>update miaosha_goods set stock=stock-1 where goos_id=&#123;#goods_id&#125; and version=#&#123;version&#125; and sock&gt;0;</code> 。这样的话，就可以保证库存不会超卖并且一次更新库存,还有注意一点这里使用了版本号的乐观锁，相比较悲观锁，它的性能较好。</p>
</li>
<li>
<p>redis预减库存</p>
<p>很多请求进来，都需要后台查询库存,这是一个频繁读的场景。可以使用redis来预减库存，在秒杀开始前可以在redis设值，比如 <code>redis.set(goodsId,100)</code>，这里预放的库存为100可以设值为常量)，每次下单成功之后，<code>Integer stock = (Integer)redis.get(goosId);</code> 然后判断sock的值，如果小于常量值就减去1。不过注意当取消的时候,需要增加库存，增加库存的时候也得注意不能大于之间设定的总库存数(查询库存和扣减库存需要原子操作，此时可以借助lua脚本)下次下单再获取库存的时候,直接从redis里面查就可以了。</p>
</li>
<li>
<p>接口限流</p>
<p>秒杀最终的本质是数据库的更新，但是有很多大量无效的请求，我们最终要做的就是如何把这些无效的请求过滤掉，防止渗透到数据库。限流的话，需要入手的方面很多：</p>
<ul>
<li>前端限流：首先第一步就是通过前端限流，用户在秒杀按钮点击以后发起请求，那么在接下来的5秒是无法点击（通过设置按钮为disable）。这一小举措开发起来成本很小，但是很有效。</li>
<li>同一个用户x秒内重复请求直接拒绝：具体多少秒需要根据实际业务和秒杀的人数而定，一般限定为10秒。具体的做法就是通过redis的键过期策略，首先对每个请求都从<code>String value = redis.get(userId);</code>。如果获取到这个value为空或者为null，表示它是有效的请求，然后放行这个请求。如果不为空表示它是重复性请求，直接丢掉这个请求。如果有效,采用<code>redis.setexpire(userId,value,10).value</code> 可以是任意值，一般放业务属性比较好,这个是设置以userId为key，10秒的过期时间（10秒后,key对应的值自动为null）。</li>
<li>令牌桶算法限流：接口限流的策略有很多，我们这里采用令牌桶算法。令牌桶算法的基本思路是每个请求尝试获取一个令牌，后端只处理持有令牌的请求，生产令牌的速度和效率我们都可以自己限定。</li>
</ul>
</li>
<li>
<p>异步下单</p>
<p>为了提升下单的效率，并且防止下单服务的失败。需要将下单这一操作进行异步处理。最常采用的办法是使用队列，队列最显著的三个优点：异步、削峰、解耦。这里可以采用rabbitmq，在后台经过了限流、库存校验之后，流入到这一步骤的就是有效请求。然后发送到队列里，队列接受消息，异步下单。下完单，入库没有问题可以用短信通知用户秒杀成功。假如失败的话，可以采用补偿机制，重试。</p>
</li>
<li>
<p>服务降级</p>
<p>假如在秒杀过程中出现了某个服务器宕机，或者服务不可用，应该做好后备工作。之前的博客里有介绍通过Hystrix进行服务熔断和降级，可以开发一个备用服务，假如服务器真的宕机了，直接给用户一个友好的提示返回，而不是直接卡死，服务器错误等生硬的反馈。</p>
</li>
</ol>
<h2 id="1-3-扫码登录流程">1.3 扫码登录流程</h2>
<p><strong>参考答案</strong></p>
<p>什么是二维码？</p>
<blockquote>
<p>二维码又称二维条码，常见的二维码为QR Code，QR全称Quick Response，是一个近几年来移动设备上超流行的一种编码方式，它比传统的Bar Code条形码能存更多的信息，也能表示更多的数据类型。-- 百度百科</p>
</blockquote>
<p>在商品上，一般都会有条形码，条形码也称为一维码，条形码只能表示一串数字。二维码要比条形码丰富很多，可以存储数字、字符串、图片、文件等，比如我们可以把 <code>www.nowcoder.com</code> 存储在二维码中，扫码二维码我们就可以获取到牛客网的地址。</p>
<p>移动端基于token的认证机制</p>
<p>在了解扫码登录原理之前，有必要先了解移动端基于 token 的认证机制，对理解扫码登录原理还是非常有帮助的。基于 token 的认证机制跟我们常用的账号密码认证方式有较大的不同，安全系数比账号密码要高，如果每次验证都传入账号密码，那么被劫持的概率就变大了。</p>
<p>基于 token 的认证机制流程图，如下图所示：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/qrcode-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>基于 token 的认证机制，只有在第一次使用需要输入账号密码，后续使用将不在输入账号密码。其实在登陆的时候不仅传入账号、密码，还传入了手机的设备信息。在服务端验证账号、密码正确后，服务端会做两件事。</p>
<ol>
<li>将账号与设备关联起来，在某种意义上，设备信息就代表着账号。</li>
<li>生成一个 token 令牌，并且在 token 与账号、设备关联，类似于key/value，token 作为 key ，账号、设备信息作为value，持久化在磁盘上。</li>
</ol>
<p>将 token 返回给移动端，移动端将 token 存入在本地，往后移动端都通过 token 访问服务端 API ，当然除了 token 之外，还需要携带设备信息，因为 token 可能会被劫持。带上设备信息之后，就算 token 被劫持也没有关系，因为设备信息是唯一的。</p>
<p>这就是基于 token 的认证机制，将账号密码换成了 token、设备信息，从而提高了安全系数，可别小看这个 token ，token 是身份凭证，在扫码登录的时候也会用到。</p>
<p>二维码扫码登录的原理</p>
<p>好了，知道了移动端基于 token 的认证机制后，接下来就进入我们的主题：二维码扫码登陆的原理。先上二维码扫码登录的流程图：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/qrcode-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>扫码登录可以分为三个阶段：待扫描、已扫描待确认、已确认。我们就来看看这三个阶段。</p>
<ol>
<li>
<p>带扫描阶段</p>
<p>待扫描阶段也就是流程图中 1~5 阶段，即生成二维码阶段，这个阶段跟移动端没有关系，是 PC 端跟服务端的交互过程。</p>
<p>首先 PC 端携带设备信息想服务端发起生成二维码请求，服务端会生成唯一的二维码 ID，你可以理解为 UUID，并且将 二维码 ID 跟 PC 设备信息关联起来，这跟移动端登录有点相似。</p>
<p>PC 端接受到二维码 ID 之后，将二维码 ID 以二维码的形式展示，等待移动端扫码。此时在 PC 端会启动一个定时器，轮询查询二维码的状态。如果移动端未扫描的话，那么一段时间后二维码将会失效。</p>
</li>
<li>
<p>已扫码待确认阶段</p>
<p>流程图中第 6 ~ 10 阶段，我们在 PC 端登录微信时，手机扫码后，PC 端的二维码会变成已扫码，请在手机端确认。这个阶段是移动端跟服务端交互的过程。</p>
<p>首先移动端扫描二维码，获取二维码 ID，然后将手机端登录的信息凭证（token）和 二维码 ID 作为参数发送给服务端，此时的手机一定是登录的，不存在没登录的情况。</p>
<p>服务端接受请求后，会将 token 与二维码 ID 关联，为什么需要关联呢？你想想，我们使用微信时，移动端退出， PC 端是不是也需要退出，这个关联就有点把子作用了。然后会生成一个一次性 token，这个 token 会返回给移动端，一次性 token 用作确认时候的凭证。</p>
</li>
</ol>
<p>然后PC 端的定时器，会轮询到二维码的状态已经发生变化，会将 PC 端的二维码更新为已扫描，请确认。</p>
<ol>
<li>
<p>已确认</p>
<p>流程图中的 第 11 ~ 15 步骤，这是扫码登录的最后阶段，移动端携带上一步骤中获取的临时 token ，确认登录，服务端校对完成后，会更新二维码状态，并且给 PC 端生成一个正式的 token ，后续 PC 端就是持有这个 token 访问服务端。</p>
<p>PC 端的定时器，轮询到了二维码状态为登录状态，并且会获取到了生成的 token ，完成登录，后续访问都基于 token 完成。</p>
<p>在服务器端会跟手机端一样，维护着 token 跟二维码、PC 设备信息、账号等信息。</p>
</li>
</ol>
<h2 id="1-4-如何实现单点登录？">1.4 如何实现单点登录？</h2>
<p><strong>参考答案</strong></p>
<p>单点登录全称Single Sign On（以下简称SSO），是指在多系统应用群中登录一个系统，便可在其他所有系统中得到授权而无需再次登录，包括单点登录与单点注销两部分。</p>
<p>登录</p>
<p>相比于单系统登录，sso需要一个独立的认证中心，只有认证中心能接受用户的用户名密码等安全信息，其他系统不提供登录入口，只接受认证中心的间接授权。间接授权通过令牌实现，sso认证中心验证用户的用户名密码没问题，创建授权令牌，在接下来的跳转过程中，授权令牌作为参数发送给各个子系统，子系统拿到令牌，即得到了授权，可以借此创建局部会话，局部会话登录方式与单系统的登录方式相同。这个过程，也就是单点登录的原理，用下图说明：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/sso-1.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>下面对上图简要描述：</p>
<ol>
<li>用户访问系统1的受保护资源，系统1发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数；</li>
<li>sso认证中心发现用户未登录，将用户引导至登录页面；</li>
<li>用户输入用户名密码提交登录申请；</li>
<li>sso认证中心校验用户信息，创建用户与sso认证中心之间的会话，称为全局会话，同时创建授权令牌；</li>
<li>sso认证中心带着令牌跳转会最初的请求地址（系统1）；</li>
<li>系统1拿到令牌，去sso认证中心校验令牌是否有效；</li>
<li>sso认证中心校验令牌，返回有效，注册系统1；</li>
<li>系统1使用该令牌创建与用户的会话，称为局部会话，返回受保护资源；</li>
<li>用户访问系统2的受保护资源；</li>
<li>系统2发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数；</li>
<li>sso认证中心发现用户已登录，跳转回系统2的地址，并附上令牌；</li>
<li>系统2拿到令牌，去sso认证中心校验令牌是否有效；</li>
<li>sso认证中心校验令牌，返回有效，注册系统2；</li>
<li>系统2使用该令牌创建与用户的局部会话，返回受保护资源。</li>
</ol>
<p>用户登录成功之后，会与sso认证中心及各个子系统建立会话，用户与sso认证中心建立的会话称为全局会话，用户与各个子系统建立的会话称为局部会话，局部会话建立之后，用户访问子系统受保护资源将不再通过sso认证中心，全局会话与局部会话有如下约束关系：</p>
<ul>
<li>局部会话存在，全局会话一定存在；</li>
<li>全局会话存在，局部会话不一定存在；</li>
<li>全局会话销毁，局部会话必须销毁。</li>
</ul>
<p>注销</p>
<p>单点登录自然也要单点注销，在一个子系统中注销，所有子系统的会话都将被销毁，用下面的图来说明：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/sso-2.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>sso认证中心一直监听全局会话的状态，一旦全局会话销毁，监听器将通知所有注册系统执行注销操作。下面对上图简要说明：</p>
<ol>
<li>用户向系统1发起注销请求；</li>
<li>系统1根据用户与系统1建立的会话id拿到令牌，向sso认证中心发起注销请求；</li>
<li>sso认证中心校验令牌有效，销毁全局会话，同时取出所有用此令牌注册的系统地址；</li>
<li>sso认证中心向所有注册系统发起注销请求；</li>
<li>各注册系统接收sso认证中心的注销请求，销毁局部会话；</li>
<li>sso认证中心引导用户至登录页面。</li>
</ol>
<p>部署</p>
<p>单点登录涉及sso认证中心与众子系统，子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求，因而子系统必须集成sso的客户端，sso认证中心则是sso服务端，整个单点登录过程实质是sso客户端与服务端通信的过程，用下图描述：</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/sso-3.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<p>sso认证中心与sso客户端通信方式有多种，这里以简单好用的httpClient为例，web service、rpc、restful api都可以。</p>
<h2 id="1-5-如何设计一个本地缓存？">1.5 如何设计一个本地缓存？</h2>
<p><strong>参考答案</strong></p>
<p>想要设计一个本地缓存，考虑点主要在数据用何种方式存储，能存储多少数据，多余的数据如何处理等几个点，下面我们来详细的介绍每个考虑点：</p>
<ol>
<li>
<p>数据结构</p>
<p>首要考虑的就是数据该如何存储，用什么数据结构存储。最简单的就直接用Map来存储数据，或者复杂的如redis一样提供了多种数据类型哈希，列表，集合，有序集合等，底层使用了双端链表，压缩列表，集合，跳跃表等数据结构。</p>
</li>
<li>
<p>对象上限</p>
<p>因为是本地缓存，内存有上限，所以一般都会指定缓存对象的数量比如1024，当达到某个上限后需要有某种策略去删除多余的数据。</p>
</li>
<li>
<p>清除策略</p>
<p>上面说到当达到对象上限之后需要有清除策略，常见的比如有LRU(最近最少使用)、FIFO(先进先出)、LFU(最近最不常用)、SOFT(软引用)、WEAK(弱引用)等策略。</p>
</li>
<li>
<p>过期时间</p>
<p>除了使用清除策略，一般本地缓存也会有一个过期时间设置，比如redis可以给每个key设置一个过期时间，这样当达到过期时间之后直接删除，采用清除策略+过期时间双重保证。</p>
</li>
<li>
<p>线程安全</p>
<p>像redis是直接使用单线程处理，所以就不存在线程安全问题。而我们现在提供的本地缓存往往是可以多个线程同时访问的，所以线程安全是不容忽视的问题，并且线程安全问题是不应该抛给使用者去保证。</p>
</li>
<li>
<p>简明的接口</p>
<p>提供一个傻瓜式的对外接口是很有必要的，对使用者来说使用此缓存不是一种负担而是一种享受，提供常用的get，put，remove，clear，getSize等方法即可。</p>
</li>
<li>
<p>是否持久化</p>
<p>这个其实不是必须的，是否需要将缓存数据持久化看需求。本地缓存如ehcache是支持持久化的，而guava是没有持久化功能的。分布式缓存如redis是有持久化功能的，memcached是没有持久化功能的。</p>
</li>
<li>
<p>阻塞机制</p>
<p>我们使用缓存的目的就是因为被缓存的数据生成比较费时，比如调用对外的接口，查询数据库，计算量很大的结果等等。这时候如果多个线程同时调用get方法获取的结果都为null，每个线程都去执行一遍费时的计算，其实也是对资源的浪费。最好的办法是只有一个线程去执行，其他线程等待，计算一次就够了。但是此功能基本上都交给使用者来处理，很少有本地缓存有这种功能。</p>
</li>
</ol>
<h1>7. Python</h1>
<h2 id="1-python-多态-封装-继承">1.python 多态 封装 继承</h2>
<p><a target="_blank" rel="noopener" href="https://www.cnblogs.com/ostrich-sunshine/p/9415698.html">https://www.cnblogs.com/ostrich-sunshine/p/9415698.html</a></p>
<h2 id="2-深拷贝与浅拷贝">2.深拷贝与浅拷贝</h2>
<p>浅拷贝：只是对另外一个变量的内存地址的拷贝，这两个变量指向同一个内存地址的变量值。<br>
深拷贝： 一个变量对另外一个变量的值拷贝。 所以当一个变量改变时候，另一个对象也会改变。</p>
<p><strong>浅拷贝</strong>只是对指针的拷贝，拷贝后两个指针指向同一个内存空间；<strong>深拷贝</strong>不断对指针进行拷贝，而且对指针指向的内容进行拷贝，经深拷贝后的指针是指向两个不同的地址空间。</p>
<ol>
<li>
<p>浅拷贝</p>
<p>对一个已知对象进行拷贝时，编译系统会自动调用一次构造函数（拷贝构造函数），如果用户未定义拷贝构造函数，则会调用默认拷贝构造函数，调用一次构造函数，调用两次析构函数，两个对象的指针成员所指内存相同，但是程序结束时该内存被释放了两次，会造成内存泄漏问题。</p>
</li>
<li>
<p>深拷贝</p>
<p>在对含有指针成员的对象进行拷贝时，必须要自己定义拷贝构造函数，使拷贝后的对象指针成员有自己的内存空间，即进行深拷贝，这样就避免了内存泄漏的发生，调用一次构造函数，一次自定义拷贝构造函数，两次析构函数。两个对象的指针成员所指内容不同。</p>
</li>
</ol>
<p><strong>结论：</strong></p>
<ul>
<li>深浅拷贝都是对源对象的复制，占用不同的内存空间</li>
<li>如果源对象只有一级目录的话，源做任何改动，不影响深浅拷贝对象</li>
<li>如果源对象不止一级目录的话，源做任何改动，都要影响浅拷贝（经过测试源对象就算有多级目录但改动一级目录，浅拷贝是不受影响的，其他级目录改动就会影响），但不影响深拷贝</li>
<li>序列对象的切片其实是浅拷贝，即只拷贝顶级的对象</li>
</ul>
<h2 id="3-Python中的数据结构">3.Python中的数据结构</h2>
<h5 id="列表list">列表list</h5>
<p>列表中的每个元素都可变的，意味着可以对每个元素进行修改和删除；<br>
列表是有序的，每个元素的位置是确定的，可以用索引去访问每个元素</p>
<h5 id="元组tuple">元组tuple</h5>
<p>Tuple一经初始化，就不能修改，没有List中的append(), insert(), pop()等修改的方法，只能对元素进行查询</p>
<h5 id="字典dict">字典dict</h5>
<p>字典中的数据必须以键值对的形式出现。 key 必须是可哈希的值。其插入和删除的时间复杂度为O(1)。</p>
<h5 id="集合set">集合set</h5>
<p>5.集合中每个元素都是无序的、不重复的任意对象</p>
<p>作者：黑城<br>
链接：<a target="_blank" rel="noopener" href="https://www.nowcoder.com/discuss/527648?type=post&amp;order=time&amp;pos=&amp;page=1&amp;ncTraceId=&amp;channel=-1&amp;source_id=search_post_nctrack">https://www.nowcoder.com/discuss/527648?type=post&amp;order=time&amp;pos=&amp;page=1&amp;ncTraceId=&amp;channel=-1&amp;source_id=search_post_nctrack</a><br>
来源：牛客网</p>
<h2 id="4-装饰器-计时器、定时器">4.装饰器 计时器、定时器</h2>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> time<br><br><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">timer1</span>(<span class="hljs-params">func</span>):</span><br>    <span class="hljs-comment"># 无参数版</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">wrapper</span>():</span><br>        start_time = time.time()<br>        func()<br>        end_time = time.time()<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;时间：%s&#x27;</span> % (end_time - start_time))<br><br>    <span class="hljs-keyword">return</span> wrapper<br><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">timer2</span>(<span class="hljs-params">func</span>):</span><br>    <span class="hljs-comment"># 参数版</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">wrapper2</span>(<span class="hljs-params">*args, **kwargs</span>):</span><br>        start_time = time.time()<br>        func(*args, **kwargs)<br>        end_time = time.time()<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;时间：%s&#x27;</span> % (end_time - start_time))<br><br>    <span class="hljs-keyword">return</span> wrapper2<br><br><span class="hljs-meta">@timer1  </span><span class="hljs-comment">#语法糖  test=timer(test)</span><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test1</span>():</span><br>    time.sleep(<span class="hljs-number">1</span>)<br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;hello world!&#x27;</span>)<br><br><span class="hljs-meta">@timer2  </span><span class="hljs-comment">#语法糖  test=timer(test)</span><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">test2</span>(<span class="hljs-params">a=<span class="hljs-number">1</span>, b=<span class="hljs-number">2</span></span>):</span><br>    time.sleep(<span class="hljs-number">1</span>)<br>    <span class="hljs-built_in">print</span>(a, b)<br><br>test1()<br>test2()<br><br>hello world!<br>时间：<span class="hljs-number">1.01173734664917</span><br><span class="hljs-number">1</span> <span class="hljs-number">2</span><br>时间：<span class="hljs-number">1.0023975372314453</span><br></code></pre></td></tr></table></figure>
<h2 id="5-import之类的知识点。">5.import之类的知识点。</h2>
<p><a target="_blank" rel="noopener" href="https://blog.csdn.net/weixin_38256474/article/details/81228492?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link&amp;depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link">https://blog.csdn.net/weixin_38256474/article/details/81228492?utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-2.no_search_link&amp;depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-2.no_search_link</a></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/70.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="这里写图片描述">
<p>不确定问的什么，但是有import相关导入</p>
<ul>
<li><code>from . import module_name</code>。导入和自己同目录下的模块。</li>
<li><code>from .package_name import module_name</code>。导入和自己同目录的包的模块。</li>
<li><code>from .. import module_name</code>。导入上级目录的模块。</li>
<li><code>from ..package_name import module_name</code>。导入位于上级目录下的包的模块。</li>
<li>当然还可以有更多的<code>.</code>，每多一个点就多往上一层目录。</li>
</ul>
<h2 id="6-装饰器，多个装饰器顺序问题">6.装饰器，多个装饰器顺序问题</h2>
<p>装饰器函数的执行顺序分为（被装饰函数）定义阶段和（被装饰函数）执行阶段<br>
<strong>在被装饰函数定义阶段，执行顺序是从最靠近函数的装饰器开始，自内而外执行</strong><br>
<strong>在被装饰函数执行阶段，执行顺序由外而内</strong><br>
需要注意的是，装饰器函数在被装饰器函数定义好的时候就立即执行了</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">A</span>(<span class="hljs-params">fn</span>):</span><br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;执行A&#x27;</span>)<br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">inner1</span>():</span><br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;开始执行inner1&#x27;</span>)<br>        fn()<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;结束inner1&#x27;</span>)<br>    <span class="hljs-keyword">return</span> inner1<br><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">B</span>(<span class="hljs-params">fn</span>):</span><br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;执行B&#x27;</span>)<br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">inner2</span>():</span><br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;开始执行inner2&#x27;</span>)<br>        fn()<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;结束inner2&#x27;</span>)<br>    <span class="hljs-keyword">return</span> inner2<br><br><span class="hljs-meta">@A</span><br><span class="hljs-meta">@B</span><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hello</span>():</span>   <span class="hljs-comment">#A(B(hello))</span><br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;hello&#x27;</span>)<br><br>hello()<br><br><span class="hljs-comment">###</span><br>执行B<br>执行A<br>开始执行inner1<br>开始执行inner2<br>hello<br>结束inner2<br>结束inner1<br></code></pre></td></tr></table></figure>
<h2 id="7-元类">7.元类</h2>
<p><a target="_blank" rel="noopener" href="https://www.cnblogs.com/ellisonzhang/p/10513238.html">https://www.cnblogs.com/ellisonzhang/p/10513238.html</a></p>
<p>元类是metaclass，首先，在python中类其实也是对象，元类就是创建类（对象）的类，所以元类就是类的类。type就是python用来创建类的元类。换句话说，按我的理解，就是我们平时代码中写的类它在代码执行的时候已经在内存中创建了一个对象了，这个对象就是元类所创建的类对象。然后我们再去用这个类对象创建实例。元类的话除了type其实也可以自定义，因为类创建对象的时候它回去寻找顶层的元类，然后通过它进行实例的创建。然后在类中加入__metaclass__可以指定元类。元类可以通过函数或者class类设定</p>
<figure class="highlight elm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs elm"><span class="hljs-type">MyClass</span> = <span class="hljs-type">MetaClass</span>()    #元类创建<br><span class="hljs-type">MyObject</span> = <span class="hljs-type">MyClass</span>()     #类创建实例<br>实际上<span class="hljs-type">MyClass</span>就是通过<span class="hljs-keyword">type</span>()来创创建出<span class="hljs-type">MyClass</span>类，它是<span class="hljs-keyword">type</span>()类的一个实例；同时<span class="hljs-type">MyClass</span>本身也是类，也可以创建出自己的实例，这里就是<span class="hljs-type">MyObject</span><br></code></pre></td></tr></table></figure>
<h2 id="8-类方法、普通方法、实例方法、静态方法">8.类方法、普通方法、实例方法、静态方法</h2>
<p>1.实例方法只能由实例调用，并且一般使用self作为实例本身在类实例化的时候进行传参；<br>
2.普通方法只能由类调用，不能由实例调用，可以在类内部被其他函数调用；<br>
3.静态方法和类方法都可以由类和实例调用。</p>
<h2 id="9-python数据类型">9.python数据类型</h2>
<p>字符串 列表 元组 字典 集合</p>
<h3 id="9-1列表和元组区别">9.1列表和元组区别</h3>
<ul>
<li>列表是动态的，长度大小不固定，可以随意的增加、删除、修改元素</li>
<li>元组是静态的，长度在初始化的时候就已经确定不能更改，更无法增加、删除、修改元素</li>
</ul>
<h5 id="list和tuple存储方式的差异">list和tuple存储方式的差异</h5>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/image-20211017224738615.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="image-20211017224738615">
<p>这里构造了一个list和一个tuple。他们存储的内容是相同的，__sizeof__方法可以打印系统分配空间的大小。可以看到他们所占用的内存空间是不同的，存储的内容相同，但是list比tuple多占用了16自己的内存。</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/image-20211017224722506.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="image-20211017224722506">
<p>list为了能够实时追踪内存的使用情况，当空间不足时以及分配额外空间，<strong>额外的多分配了内存</strong>，而且还需要存储指针，指向对应的元素。</p>
<p>我们可以看到，为了减小每次增加 / 删减操作时空间分配的开销，Python 每次分配空间时都会额外多分配一些，这样的机制（over-allocating）保证了其操作的高效性：增加 / 删除的时间复杂度均为 O(1)。但是对于元组，情况就不同了。元组长度大小固定，元素不可变，所以存储空间固定。</p>
<h2 id="10-python-垃圾回收-GC">10.python 垃圾回收 GC</h2>
<p>GC 引用计数，还会有标志-清除和分代回收辅助，标志清除可以解决循环引用，分代回收(年轻 青中年 老年)能够提升标志清除的回收效率，不用全部遍历所有链表对象，而是通过划分不同世代，设计不同的回收频率，分代回收也是因为在内存中对象存在时间越久越不可能是垃圾，应该越少去收集。</p>
<h2 id="11-is和-区别">11.is和==区别</h2>
<p>is比较的是两个对象的地址值，也就是说两个对象是否为同一个实例对象；而==比较的是对象的值是否相等，其调用了对象的_<em>eq</em>_()方法</p>
<h2 id="12-生成器迭代器">12.生成器迭代器</h2>
<ul>
<li>可迭代对象实现了 __ iter __ 方法或者实现 __ getitem __方法而且其参数从0开始索引。</li>
<li>使用iter()函数判断可迭代对象更准确</li>
<li>任何序列都是可迭代对象</li>
<li>迭代器对象实现了 __ iter __ 和 __ next __方法。</li>
<li>迭代器是一个可以记住遍历位置的对象，其内部有一个状态用于记录迭代所在的位置，以便下次迭代时候能取出正确的元素</li>
<li>检查对象是否为迭代器最好的方式是调用isinstance()方法。</li>
</ul>
<p>生成器(generator)</p>
<p>一个包含了yield关键字的函数就是一个生成器，该函数也叫生成器函数。当生成器函数被调用时，在函数体中的代码不会被执行，而会返回一个迭代器。每次请求一个值，就会执行生成器中代码，直到遇到一个yield表达式或return语句。yield表达式表示要生成一个值，return语句表示要停止生成器。换句话说，生成器是由两部分组成，生成器的函数和生成器的迭代器。生成器的函数是用def语句定义的，包含yield部分；生成器的迭代器是这个函数返回的部分。二者合起来叫做生成器。</p>
<h2 id="13-Python-GIL锁（全局解释器锁）">13.Python GIL锁（全局解释器锁）</h2>
<p>一个进程中含有两个线程，分别为线程0和线程1，两个线程全都引用对象a。当两个线程同时对a发生引用（并未修改，不需要使用同步性原语），就会发生同时修改对象a的引用计数器，造成计数器引用少于实质性的引用，当进行垃圾回收时，造成错误异常。因此，需要一把全局锁（即为GIL）来保证对象引用计数的正确性和安全性。</p>
<h2 id="14-Python了解哪些底层原理">14.Python了解哪些底层原理</h2>
<p>python 如何内存管理机制</p>
<p>从三方面讲述：1.对象的引用计数机制、2.垃圾回收机制、3。内存池机制</p>
<h5 id="一、对象的引用计数机制"><strong>一、对象的引用计数机制</strong></h5>
<p><strong>Python内部使用引用计数，来保持追踪内存中的对象，所有对象都有引用计数。</strong></p>
<p><strong>引用计数增加的情况：</strong></p>
<p><strong>1，一个对象分配一个新名称</strong></p>
<p><strong>2，将其放入一个容器中（如列表、元组或字典）</strong></p>
<p><strong>引用计数减少的情况：</strong></p>
<p><strong>1，使用del语句对对象别名显示的销毁</strong></p>
<p><strong>2，引用超出作用域或被重新赋值</strong></p>
<p><strong>sys.getrefcount( )函数可以获得对象的当前引用计数</strong></p>
<p><strong>多数情况下，引用计数比你猜测得要大得多。对于不可变数据（如数字和字符串），解释器会在程序的不同部分共享内存，以便节约内存。</strong></p>
<h5 id="二、垃圾回收"><strong>二、垃圾回收</strong></h5>
<p><strong>1，当一个对象的引用计数归零时，它将被垃圾收集机制处理掉。</strong></p>
<p><strong>2，当两个对象a和b相互引用时，del语句可以减少a和b的引用计数，并销毁用于引用底层对象的名称。然而由于每个对象都包含一个对其他对象的应用，因此引用计数不会归零，对象也不会销毁。（从而导致内存泄露）。为解决这一问题，解释器会定期执行一个循环检测器，搜索不可访问对象的循环并删除它们。</strong></p>
<h5 id="三、内存池机制"><strong>三、内存池机制</strong></h5>
<p><strong>Python提供了对内存的垃圾收集机制，但是它将不用的内存放到内存池而不是返回给操作系统。</strong></p>
<p><strong>1，Pymalloc机制。为了加速Python的执行效率，Python引入了一个内存池机制，用于管理对小块内存的申请和释放。</strong></p>
<p><strong>2，Python中所有小于256个字节的对象都使用pymalloc实现的分配器，而大的对象则使用系统的malloc。</strong></p>
<p><strong>3，对于Python对象，如整数，浮点数和List，都有其独立的私有内存池，对象间不共享他们的内存池。也就是说如果你分配又释放了大量的整数，用于缓存这些整数的内存就不能再分配给浮点数。</strong></p>
<h2 id="15-如果进程内存使用过高，可能有几个G，在Python该如何查找具体是哪一个对象占用比较高，通过哪个模块可以找到这些信息">15.如果进程内存使用过高，可能有几个G，在Python该如何查找具体是哪一个对象占用比较高，通过哪个模块可以找到这些信息</h2>
<p>Python 在 sys 模块中提供函数 getsizeof 来计算 Python 对象的大小。</p>
<p>sys.getsizeof(object[, default])</p>
<p>以字节(byte)为单位返回对象大小。 这个对象可以是任何类型的对象。 所以内置对象都能返回正确的结果 但不保证对第三方扩展有效，因为和具体实现相关。</p>
<p>getsizeof() 调用对象的_<em>sizeof</em>_ 方法， 如果对象由垃圾收集器管理， 则会加上额外的垃圾收集器开销。</p>
<p>通过_<em>slot</em>_()可以节省内存</p>
<h2 id="16-单例模式（多种方法-Python实现）">16.单例模式（多种方法 Python实现）</h2>
<h5 id="1-使用模块">1.使用模块</h5>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/image-20211017235927715.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="image-20211017235927715">
<h5 id="2-使用装饰器">2.使用装饰器</h5>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">Singleton</span>(<span class="hljs-params">cls</span>):</span><br>    _instance = &#123;&#125;<br><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_singleton</span>(<span class="hljs-params">*args, **kargs</span>):</span><br>        <span class="hljs-keyword">if</span> cls <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> _instance:<br>            _instance[cls] = cls(*args, **kargs)<br>        <span class="hljs-keyword">return</span> _instance[cls]<br><br>    <span class="hljs-keyword">return</span> _singleton<br><br><br><span class="hljs-meta">@Singleton</span><br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">A</span>(<span class="hljs-params"><span class="hljs-built_in">object</span></span>):</span><br>    a = <span class="hljs-number">1</span><br><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, x=<span class="hljs-number">0</span></span>):</span><br>        self.x = x<br><br><br>a1 = A(<span class="hljs-number">2</span>)<br>a2 = A(<span class="hljs-number">3</span>)<br></code></pre></td></tr></table></figure>
<h5 id="3-使用类-一般情况，大家以为这样就完成了单例模式，但是这样当使用多线程时会存在问题-当-init-进行io操作后那么就无法实现单例了，所以要加锁">3.使用类(###一般情况，大家以为这样就完成了单例模式，但是这样当使用多线程时会存在问题,当__init__进行io操作后那么就无法实现单例了，所以要加锁)</h5>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Singleton</span>(<span class="hljs-params"><span class="hljs-built_in">object</span></span>):</span><br><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span><br>        <span class="hljs-keyword">pass</span><br><br><span class="hljs-meta">    @classmethod</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">instance</span>(<span class="hljs-params">cls, *args, **kwargs</span>):</span><br>        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-built_in">hasattr</span>(Singleton, <span class="hljs-string">&quot;_instance&quot;</span>):<br>            Singleton._instance = Singleton(*args, **kwargs)<br>        <span class="hljs-keyword">return</span> Singleton._instance<br> <br></code></pre></td></tr></table></figure>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-comment">#####解决办法：加锁！未加锁部分并发执行,加锁部分串行执行,速度降低,但是保证了数据安全</span><br><br><span class="hljs-keyword">import</span> time<br><span class="hljs-keyword">import</span> threading<br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Singleton</span>(<span class="hljs-params"><span class="hljs-built_in">object</span></span>):</span><br>    _instance_lock = threading.Lock()<br><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span><br>        time.sleep(<span class="hljs-number">1</span>)<br><br><span class="hljs-meta">    @classmethod</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">instance</span>(<span class="hljs-params">cls, *args, **kwargs</span>):</span><br>        <span class="hljs-keyword">with</span> Singleton._instance_lock:<br>            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-built_in">hasattr</span>(Singleton, <span class="hljs-string">&quot;_instance&quot;</span>):<br>                Singleton._instance = Singleton(*args, **kwargs)<br>        <span class="hljs-keyword">return</span> Singleton._instance<br><br><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task</span>(<span class="hljs-params">arg</span>):</span><br>    obj = Singleton.instance()<br>    <span class="hljs-built_in">print</span>(obj)<br><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">10</span>):<br>    t = threading.Thread(target=task,args=[i,])<br>    t.start()<br>time.sleep(<span class="hljs-number">20</span>)<br>obj = Singleton.instance()<br><span class="hljs-built_in">print</span>(obj)<br></code></pre></td></tr></table></figure>
<h5 id="4-基于-new-方法实现">4.基于__new__方法实现</h5>
<h5 id="简单实现方法">简单实现方法</h5>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Singleton</span>:</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__new__</span>(<span class="hljs-params">cls, *args, **kwargs</span>):</span><br>        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-built_in">hasattr</span>(cls, <span class="hljs-string">&quot;_instance&quot;</span>):<br>            cls._instance = <span class="hljs-built_in">super</span>().__new__(cls, *args, **kwargs)<br>        <span class="hljs-keyword">return</span> cls._instance<br><br>s1 = Singleton()<br>s2 = Singleton()<br><span class="hljs-built_in">print</span>(s1 <span class="hljs-keyword">is</span> s2)<br></code></pre></td></tr></table></figure>
<h5 id="复杂实现方法，加入了锁">复杂实现方法，加入了锁</h5>
<p>当我们实现单例时，<strong>为了保证线程安全需要在内部加入锁</strong></p>
<p>我们知道，当我们实例化一个对象时，是<strong>先执行了类的__new__方法</strong>（我们没写时，默认调用object.<strong>new</strong>），<strong>实例化对象</strong>；然后<strong>再执行类的__init__方法</strong>，对这个对象进行初始化，所有我们可以基于这个，实现单例模式</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> threading<br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Singleton</span>(<span class="hljs-params"><span class="hljs-built_in">object</span></span>):</span><br>    _instance_lock = threading.Lock()<br><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span><br>        <span class="hljs-keyword">pass</span><br><br><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__new__</span>(<span class="hljs-params">cls, *args, **kwargs</span>):</span><br>        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-built_in">hasattr</span>(Singleton, <span class="hljs-string">&quot;_instance&quot;</span>):<br>            <span class="hljs-keyword">with</span> Singleton._instance_lock:<br>                <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-built_in">hasattr</span>(Singleton, <span class="hljs-string">&quot;_instance&quot;</span>):<br>                    Singleton._instance = <span class="hljs-built_in">object</span>.__new__(cls)  <br>        <span class="hljs-keyword">return</span> Singleton._instance<br><br>obj1 = Singleton()<br>obj2 = Singleton()<br><span class="hljs-built_in">print</span>(obj1,obj2)<br><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task</span>(<span class="hljs-params">arg</span>):</span><br>    obj = Singleton()<br>    <span class="hljs-built_in">print</span>(obj)<br><br><span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">10</span>):<br>    t = threading.Thread(target=task,args=[i,])<br>    t.start()<br></code></pre></td></tr></table></figure>
<h5 id="5-基于metaclass方式实现">5.基于metaclass方式实现</h5>
<h6 id="相关知识">相关知识</h6>
<figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs awk"><span class="hljs-string">&quot;&quot;&quot;</span><br><span class="hljs-string">1.类由type创建，创建类时，type的__init__方法自动执行，类() 执行type的 __call__方法(类的__new__方法,类的__init__方法)</span><br><span class="hljs-string">2.对象由类创建，创建对象时，类的__init__方法自动执行，对象()执行类的 __call__ 方法</span><br><span class="hljs-string">&quot;&quot;&quot;</span><br></code></pre></td></tr></table></figure>
<p>例子：</p>
<figure class="highlight angelscript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs angelscript"><span class="hljs-keyword">class</span> <span class="hljs-symbol">Foo:</span><br><span class="hljs-symbol">    <span class="hljs-symbol">def</span></span> <span class="hljs-symbol">__init__</span>(<span class="hljs-symbol">self</span>):<br>        <span class="hljs-symbol">pass</span><br><br>    <span class="hljs-symbol">def</span> <span class="hljs-symbol">__call__</span>(<span class="hljs-symbol">self, </span>*<span class="hljs-symbol">args, </span>**<span class="hljs-symbol">kwargs</span>):<br>        <span class="hljs-symbol">pass</span><br><br><span class="hljs-symbol">obj</span> = <span class="hljs-symbol">Foo</span>()<br># 执行<span class="hljs-symbol">type</span>的 <span class="hljs-symbol">__call__</span> 方法，调用 <span class="hljs-symbol">Foo</span>类（是<span class="hljs-symbol">type</span>的对象）的 <span class="hljs-symbol">__new__</span>方法，用于创建对象，然后调用 <span class="hljs-symbol">Foo</span>类（是<span class="hljs-symbol">type</span>的对象）的 <span class="hljs-symbol">__init__</span>方法，用于对对象初始化。<br><br><span class="hljs-symbol">obj</span>()    # 执行<span class="hljs-symbol">Foo</span>的 <span class="hljs-symbol">__call__</span> 方法    <br></code></pre></td></tr></table></figure>
<h6 id="元类的使用"><strong>元类的使用</strong></h6>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SingletonType</span>(<span class="hljs-params"><span class="hljs-built_in">type</span></span>):</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self,*args,**kwargs</span>):</span><br>        <span class="hljs-built_in">super</span>(SingletonType,self).__init__(*args,**kwargs)<br><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__call__</span>(<span class="hljs-params">cls, *args, **kwargs</span>):</span> <span class="hljs-comment"># 这里的cls，即Foo类</span><br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;cls&#x27;</span>,cls)<br>        obj = cls.__new__(cls,*args, **kwargs)<br>        cls.__init__(obj,*args, **kwargs) <span class="hljs-comment"># Foo.__init__(obj)</span><br>        <span class="hljs-keyword">return</span> obj<br><br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Foo</span>(<span class="hljs-params">metaclass=SingletonType</span>):</span> <span class="hljs-comment"># 指定创建Foo的type为SingletonType</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self，name</span>):</span><br>        self.name = name<br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__new__</span>(<span class="hljs-params">cls, *args, **kwargs</span>):</span><br>        <span class="hljs-keyword">return</span> <span class="hljs-built_in">object</span>.__new__(cls)<br><br>obj = Foo(<span class="hljs-string">&#x27;xx&#x27;</span>)<br></code></pre></td></tr></table></figure>
<h6 id="实现单例模式">实现单例模式</h6>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> threading<br><br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SingletonType</span>(<span class="hljs-params"><span class="hljs-built_in">type</span></span>):</span><br>    _instance_lock = threading.Lock()<br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__call__</span>(<span class="hljs-params">cls, *args, **kwargs</span>):</span><br>        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-built_in">hasattr</span>(cls, <span class="hljs-string">&quot;_instance&quot;</span>):<br>            <span class="hljs-keyword">with</span> SingletonType._instance_lock:<br>                <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-built_in">hasattr</span>(cls, <span class="hljs-string">&quot;_instance&quot;</span>):<br>                    cls._instance = <span class="hljs-built_in">super</span>(SingletonType,cls).__call__(*args, **kwargs)<br>        <span class="hljs-keyword">return</span> cls._instance<br><br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Foo</span>(<span class="hljs-params">metaclass=SingletonType</span>):</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self,name</span>):</span><br>        self.name = name<br><br><br>obj1 = Foo(<span class="hljs-string">&#x27;name&#x27;</span>)<br>obj2 = Foo(<span class="hljs-string">&#x27;name&#x27;</span>)<br><span class="hljs-built_in">print</span>(obj1,obj2)<br></code></pre></td></tr></table></figure>
<h2 id="17-Python回收内存管理机制，会不会出现回收不及时，程序运行过程中内存泄露该如何排查-内存泄漏排查">17.Python回收内存管理机制，会不会出现回收不及时，程序运行过程中内存泄露该如何排查 内存泄漏排查</h2>
<p>首先通过任务管理器查看进程情况，然后使用相关工具进行排查或者在代码中加入gc模块进行记录，查看</p>
<p>尽量少通过循环引用组织数据，可以改用weakref做弱引用或者用id之类的句柄访问对象<br>
<strong>通过gc模块的接口可以检查出每次垃圾回收有哪些对象不能自动处理，再逐个逐个处理</strong></p>
<h2 id="18-namespace和entrypoint">18.namespace和entrypoint</h2>
<p>namespace 命名空间 <a target="_blank" rel="noopener" href="https://www.runoob.com/python3/python3-namespace-scope.html">https://www.runoob.com/python3/python3-namespace-scope.html</a></p>
<p>我觉得namespace就是从名称到对象的映射，通过字典进行存储，把变量和引用的对象构建字典便于获取</p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/image-20211018151150496.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="image-20211018151150496">
<p>entrypoint   实际是一张匹配表。匹配简短指令和具体的python函数的执行路径。有点快捷方式的概念。</p>
<p>在具体就没搜到了</p>
<h2 id="19-slots-（）是用来定义什么的">19._<em>slots</em>_（）是用来定义什么的</h2>
<p><a target="_blank" rel="noopener" href="https://blog.csdn.net/weixin_40318474/article/details/110856668">https://blog.csdn.net/weixin_40318474/article/details/110856668</a></p>
<p>__slots__是什么？它又有什么功能呢？</p>
<p>在这里，我们首先得了解，在python中是默认用字典来保存对象的实例属性的，这样做的<strong>好处是允许我们在允许的任意时刻去设置新的属性</strong>，但是<strong>坏处是不能够在对象创建的时候分配固定的内存空间来保存所有的属性，造成的后果是创建很多个实例对象的时候会浪费很多的内存空间。</strong></p>
<p>__slots__属性就是用来解决这个问题的，它用<strong>来限制实例的属性字段，在创建大量对象的情况下可以减少内存的占用。</strong></p>
<p>换句话说，_<em>slots</em>_ 用来限制当前类的实例属性的</p>
<p>具体的用法如下：</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Test</span>(<span class="hljs-params"><span class="hljs-built_in">object</span></span>):</span><br>	__slots__ = [<span class="hljs-string">&quot;name&quot;</span>, <span class="hljs-string">&quot;age&quot;</span>]<br>	<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, name, age</span>):</span><br>		self.name = name<br>		self.age = age<br></code></pre></td></tr></table></figure>
<p>补充：</p>
<p>__slots__限制了属性值，添加__slots__元组之外的属性会报错！<br>
__slots__限制的是实例属性的添加，不限制类属性的添加！</p>
<h2 id="20-Python-协程原理">20.Python 协程原理</h2>
<p>协程拥有自己的寄存器上下文和栈。协程调度切换时，将寄存器上下文和栈保存到其他地方，在切回来的时候，恢复先前保存的寄存器上下文和栈。因此：协程能保留上一次调用时的状态（即所有局部状态的一个特定组合），每次过程重入时，就相当于进入上一次调用的状态，换种说法：进入上一次离开时所处逻辑流的位置。</p>
<p>协程相关代码操作等：<a target="_blank" rel="noopener" href="https://blog.csdn.net/soonfly/article/details/78361819">https://blog.csdn.net/soonfly/article/details/78361819</a></p>
<h2 id="21-python切片">21.python切片</h2>
<p>slice 切片能够从某个对象中抽取部分值的操作，然后切片后会产生新的对象。</p>
<p>切片操作<strong>不是列表特有的</strong>，python<strong>中的有序序列都支持切片</strong>，如<strong>字符串，元组</strong>。</p>
<p>总共三种对象支持：list tuple string</p>
<p>切片的返回结果类型和切片对象类型一致，返回的是切片对象的子序列，如：对一个列表切片返回一个列表，</p>
<p>字符串切片返回字符串。</p>
<p>切片生成的子序列元素是源版的拷贝。因此切片是一种浅拷贝。</p>
<h2 id="22-new-和-init-的区别？">22.___new__和__init__的区别？</h2>
<ul>
<li>__new__是在实例创建<strong>之前</strong>被调用的，因为它的任务就是创建实例然后返回该实例对象，是个静态方法。</li>
<li>__init__是当实例对象创建完成后被调用的，然后设置对象属性的一些初始值，通常用在初始化一个类实例的时候。是一个实例方法。</li>
</ul>
<p>__new__先被调用，__init__后被调用，__new__的返回值（实例）将传递给__init__方法的第一个参数，然后__init__给这个实例设置一些参数。</p>
<h2 id="23-Python-对象生命周期">23.Python 对象生命周期</h2>
<p>对象从诞生到消亡的过程</p>
<pre><code>对象被创建时, 会在内存中分配相应的内存空间进行存储

对象不再使用, 为了节约内存, 就会把这个对象释放
</code></pre>
<p>__new__创建对象时调用该方法<br>
·      __new__通常用于控制生成一个新实例的过程（用于给这个对象分配内存的方法）。它是类级别的方法</p>
<p>·      __new__至少要有一个参数cls，代表要实例化的类，此参数在实例化时由Python 解释器自动提供</p>
<p>·      __new__必须要有返回值，返回实例化出来的实例</p>
<pre><code>   return父类__new__出来的实例，或者直接是object的__new__出来的实例
</code></pre>
<p>·      通过拦截这个方法, 可以修改对象的创建过程</p>
<p>·      比如:单例设计模式</p>
<p>2.__init__对象初始化</p>
<p>·     每个对象实例化的时候，都会自动执行这个方法，__new__生成实例后才执行初始化，</p>
<pre><code>  实例级别的
</code></pre>
<p>·     可以在这个方法里面，初始化一些实例属性</p>
<p>3.__del__对象释放时调用该方法</p>
<p>·     当对象被释放的时候调用这个方法，如执行<code>del 实例</code>，同样会调用该方法</p>
<p>·     可在这个方法中清理资源<br>
————————————————<br>
版权声明：本文为CSDN博主「凤凰谷1」的原创文章，遵循CC 4.0 BY-SA版权协议，转载请附上原文出处链接及本声明。<br>
原文链接：<a target="_blank" rel="noopener" href="https://blog.csdn.net/u011089760/article/details/89986277">https://blog.csdn.net/u011089760/article/details/89986277</a></p>
<h2 id="24-Python-列表和元组，字典、集合的底层实现">24.Python 列表和元组，字典、集合的底层实现</h2>
<p><a target="_blank" rel="noopener" href="https://www.jb51.net/article/207224.htm">https://www.jb51.net/article/207224.htm</a></p>
<p>列表：</p>
<p>list 本质上是一个长度可变的连续数组。其中 ob_item 是一个指针列表，里边的每一个指针都指向列表中的元素，而 allocated 则用于存储该列表目前已被分配的空间大小。</p>
<p>元组：和列表相似，本质上也是数组不过空间大小固定，因为元组不可变对象</p>
<ul>
<li>
<pre><code class="language-c">typedef struct &#123;
PyObject_VAR_HEAD
PyObject *ob_item[1];
/* ob_item contains space for 'ob_size' elements.

* Items must normally not be NULL, except during construction when
* the tuple is not yet visible outside the function that builds it.     */&#125;
  PyTupleObject;
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br></pre></td><td class="code"><pre><code class="hljs python"><br>字典：<br><br>CPython使用伪随机探测(pseudo-random probing)的**散列表(<span class="hljs-built_in">hash</span> table)**作为字典的底层数据结构。由于这个实现细节，**只有可哈希的对象才能作为字典的键。**<br><br> Python中所有不可变的内置类型都是可哈希的。可变类型（如列表，字典和集合）就是不可哈希的，因此不能作为字典的键。<br><br>集合：<br><br>集合是一种鲁棒性很好的数据结构，当元素顺序的重要性不如元素的唯一性和测试元素是否包含在集合中的效率时，大部分情况下这种数据结构极其有用。<br><br>python的内置集合类型有两种：<br><br><span class="hljs-built_in">set</span>(): 一种可变的、无序的、有限的集合，其元素是唯一的、不可变的（可哈希的）对象。<br><br><span class="hljs-built_in">frozenset</span>(): 一种不可变的、可哈希的、无序的集合，其元素是唯一的，不可变的哈希对象。<br><br>Python中集合和字典非常相似。事实上，**集合被实现为带有空值的字典****，只有键才是实际的集合元素**。此外，集合还利用这种没有值的映射做了其它的优化。<br><br>由于这一点，可以快速的向集合中添加元素、删除元素、检查元素是否存在。平均时间复杂度为O(<span class="hljs-number">1</span>),最坏的事件复杂度是O(n)。<br><br><br><br><br><br><span class="hljs-comment">## 25.写一个类，可以读数据和写数据 </span><br><br>用到<span class="hljs-built_in">open</span>  和 <span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>可能还会问<span class="hljs-built_in">open</span> 和 <span class="hljs-keyword">with</span>是什么 如何使用 等等<br><br>https://www.cnblogs.com/ymjyqsx/p/<span class="hljs-number">6554817.</span>html<br><br><span class="hljs-comment">##### open()</span><br><br><span class="hljs-comment">##### with open() ... as ..</span><br><br>```python<br><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">handle_file</span>:</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, file_path</span>):</span><br>        self.file_path = file_path<br><br><span class="hljs-meta">    @classmethod</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">write_file</span>(<span class="hljs-params">cls, file_path, content</span>):</span><br>        <span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(file_path, <span class="hljs-string">&#x27;a&#x27;</span>) <span class="hljs-keyword">as</span> fp:<br>            fp.write(content)<br><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">readContent</span>(<span class="hljs-params">self</span>):</span><br>        <span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(self.file_path) <span class="hljs-keyword">as</span> fp:<br>            <span class="hljs-keyword">return</span> fp.read()<br><br><span class="hljs-meta">    @staticmethod</span><br>    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">read_file</span>(<span class="hljs-params">file_path, line_nbr</span>):</span><br>        <span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(file_path, <span class="hljs-string">&#x27;r&#x27;</span>) <span class="hljs-keyword">as</span> fp:<br>            strList = fp.readlines()<br>            <span class="hljs-built_in">print</span>(strList)<br>            <span class="hljs-keyword">if</span> strList[line_nbr - <span class="hljs-number">1</span>] == <span class="hljs-string">&#x27;\n&#x27;</span>:<br>                <span class="hljs-built_in">print</span>(<span class="hljs-string">&#x27;error&#x27;</span>)<br>            <span class="hljs-keyword">else</span>:<br>                <span class="hljs-keyword">return</span> strList[line_nbr - <span class="hljs-number">1</span>]<br><br><br>s = <span class="hljs-string">&quot;1ssdfsdf\n2dfsdfs\n3sdfsdf\n&quot;</span><br><br>operation = handle_file(<span class="hljs-string">&#x27;handlefile.txt&#x27;</span>)<br><br><span class="hljs-comment"># 写入内容（静态方法）</span><br>handle_file.write_file(<span class="hljs-string">&#x27;handlefile.txt&#x27;</span>, s)<br><br><span class="hljs-comment"># 读取文件内容（类方法）</span><br><span class="hljs-built_in">print</span>(operation.readContent())<br><br><span class="hljs-comment"># 读取内容指定行（静态方法）</span><br><span class="hljs-built_in">print</span>(handle_file.read_file(<span class="hljs-string">&#x27;handlefile.txt&#x27;</span>, <span class="hljs-number">4</span>))<br><br><span class="hljs-comment">#　结果</span><br><span class="hljs-comment"># 1ssdfsdf</span><br><span class="hljs-comment"># 2dfsdfs</span><br><span class="hljs-comment"># 3sdfsdf</span><br><span class="hljs-comment"># </span><br><span class="hljs-comment"># 1ssdfsdf</span><br><span class="hljs-comment"># 2dfsdfs</span><br><span class="hljs-comment"># 3sdfsdf</span><br><span class="hljs-comment"># </span><br><span class="hljs-comment"># [&#x27;1ssdfsdf\n&#x27;, &#x27;2dfsdfs\n&#x27;, &#x27;3sdfsdf\n&#x27;, &#x27;\n&#x27;, &#x27;1ssdfsdf\n&#x27;, &#x27;2dfsdfs\n&#x27;, &#x27;3sdfsdf\n&#x27;]</span><br><span class="hljs-comment"># error</span><br><span class="hljs-comment"># None</span><br><br></code></pre></td></tr></table></figure>





</code></pre>
</li>
</ul>
<h1>8. 数据结构与算法</h1>
<h2 id="1-数组和链表的区别">1.数组和链表的区别</h2>
<p><strong>不同：链表是链式的存储结构；数组是顺序的存储结构。</strong></p>
<p>链表通过指针来连接元素与元素，数组则是把所有元素按次序依次存储。</p>
<p>链表的插入删除元素相对数组较为简单，不需要移动元素，且较为容易实现长度扩充，但是寻找某个元素较为困难；</p>
<p>数组寻找某个元素较为简单，但插入与删除比较复杂，由于最大长度需要再编程一开始时指定，故当达到最大长度时，扩充长度不如链表方便。<br>
<strong>相同：两种结构均可实现数据的顺序存储，构造出来的模型呈线性结构。</strong></p>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/image-20211016161642068.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="image-20211016161642068">
<h2 id="2-排序算法">2.排序算法</h2>
<img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/849589-20180402133438219-1946132192.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img">
<h3 id="口排一下快速排序吧">口排一下快速<a href="">排序</a>吧</h3>
<p>快速排序使用分治法来把一个串（list）分为两个子串（sub-lists）。具体算法描述如下：</p>
<ul>
<li>从数列中挑出一个元素，称为 “基准”（pivot）；</li>
<li>重新排序数列，所有元素比基准值小的摆放在基准前面，所有元素比基准值大的摆在基准的后面（相同的数可以到任一边）。在这个分区退出之后，该基准就处于数列的中间位置。这个称为分区（partition）操作；</li>
<li>递归地（recursive）把小于基准值元素的子数列和大于基准值元素的子数列排序。</li>
</ul>
<h3 id="快速排序-python-代码">快速排序 python 代码</h3>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">quickSort</span>(<span class="hljs-params">arr, low=<span class="hljs-literal">None</span>, high=<span class="hljs-literal">None</span></span>):</span><br>    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(arr) == <span class="hljs-number">0</span>:<br>        <span class="hljs-keyword">return</span> arr<br>    low = <span class="hljs-number">0</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-built_in">isinstance</span>(low, (<span class="hljs-built_in">int</span>, <span class="hljs-built_in">float</span>)) <span class="hljs-keyword">else</span> low<br>    high = <span class="hljs-built_in">len</span>(arr)-<span class="hljs-number">1</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-built_in">isinstance</span>(high, (<span class="hljs-built_in">int</span>, <span class="hljs-built_in">float</span>)) <span class="hljs-keyword">else</span> high<br><br>    <span class="hljs-keyword">if</span> low &lt; high:<br>        partitionIndex = partition(arr, low, high)<br>        quickSort(arr, low, partitionIndex-<span class="hljs-number">1</span>)<br>        quickSort(arr, partitionIndex+<span class="hljs-number">1</span>, high)<br>    <span class="hljs-keyword">return</span> arr<br><br><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">partition</span>(<span class="hljs-params">arr, left, right</span>):</span><br>    pivot = left<br>    index = pivot + <span class="hljs-number">1</span><br>    i = index<br>    <span class="hljs-keyword">while</span> i &lt;= right:<br>        <span class="hljs-keyword">if</span> arr[i] &lt; arr[pivot]:<br>            swap(arr, i, index)<br>            index += <span class="hljs-number">1</span><br>        i += <span class="hljs-number">1</span><br>    swap(arr, index-<span class="hljs-number">1</span>, pivot)<br>    <span class="hljs-keyword">return</span> index - <span class="hljs-number">1</span><br><br><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">swap</span>(<span class="hljs-params">arr, i, j</span>):</span><br>    arr[i], arr[j] = arr[j], arr[i]<br><br><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">&#x27;__main__&#x27;</span>:<br>    nums = [<span class="hljs-number">15</span>, <span class="hljs-number">3</span>, <span class="hljs-number">44</span>, <span class="hljs-number">1765</span>, <span class="hljs-number">8453</span>, <span class="hljs-number">23</span>]<br>    <span class="hljs-comment"># nums = [1, 2, 3, 4, 5, 6]</span><br>    res = quickSort(nums)<br>    <span class="hljs-built_in">print</span>(res)<br></code></pre></td></tr></table></figure>
<h2 id="3-python-列表分组">3.python 列表分组</h2>
  <img src="/crystal575/2021/11/05/%E9%9D%A2%E7%BB%8F%E6%B1%87%E6%80%BB/99ABFA5E0BDA0A1F47325814495FE2F9.png" srcset="/crystal575/img/loading.gif" lazyload class="" title="img"> 
<h2 id="4-过河问题，ABCD四个人，单独过河分别需要1，2，5，10，最多两个人同时过，并且只有一个手电筒，每次都需要电筒，两人过河按慢的时间算，问最少几分钟？（17分钟）如果有n个人过河，每个人过河的时间不定，怎么用算法实现。">4.过河问题，ABCD四个人，单独过河分别需要1，2，5，10，最多两个人同时过，并且只有一个手电筒，每次都需要电筒，两人过河按慢的时间算，问最少几分钟？（17分钟）如果有n个人过河，每个人过河的时间不定，怎么用<a href="">算法</a>实现。</h2>
<p>解答：</p>
<ol>
<li>A、B先过———-&gt;2分钟（A、B在对岸）</li>
<li>A回来—————&gt;2+1=3分钟（B在对岸）</li>
<li>C、D过去———&gt;2+1+10=13分钟（B、C、D在对岸）</li>
<li>B回来—————&gt;2+1+10+2=15分钟（C、D在对岸）</li>
<li>A、B过去———-&gt;2+1+10+2+2=17分钟（A、B、C、D在对岸）</li>
</ol>
<h2 id="5-只用2GB内存在20亿个整数中找到出现次数最多的数">5.只用2GB内存在20亿个整数中找到出现次数最多的数</h2>
<p><a target="_blank" rel="noopener" href="https://blog.csdn.net/liyutaogege/article/details/104394490">https://blog.csdn.net/liyutaogege/article/details/104394490</a></p>
<p>2^32 = 4GB = 4,294,967,296 byte</p>
<p>一个整数4字节，hashtable里的一条记录包含key value并且都是整数那么就是8字节，所以</p>
<p>4GB = 四十多亿 字节 能够存储 5亿多条的hashtable记录</p>
<p>分治 加 hash表</p>
<h2 id="6-给定a、b两个文件，各存放50亿个url，每个url各占64字节，内存限制是4G，让你找出a、b文件共同的url">6.给定a、b两个文件，各存放50亿个url，每个url各占64字节，内存限制是4G，让你找出a、b文件共同的url?</h2>
<p><a target="_blank" rel="noopener" href="https://blog.csdn.net/qq_41946557/article/details/102708186?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-4.no_search_link&amp;spm=1001.2101.3001.4242.4">https://blog.csdn.net/qq_41946557/article/details/102708186?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-4.no_search_link&amp;spm=1001.2101.3001.4242.4</a></p>
<p>分治 加 hash方法</p>
<p>如果找最大最小用最小堆这种</p>
<p>优化方法使用字典树</p>
<p>7.TOP K 问题</p>

            </div>
            <hr>
            <div>
              <div class="post-metas mb-3">
                
                  <div class="post-meta mr-3">
                    <i class="iconfont icon-category"></i>
                    
                      <a class="hover-with-bg" href="/crystal575/categories/%E5%90%8E%E7%AB%AF/">后端</a>
                    
                  </div>
                
                
                  <div class="post-meta">
                    <i class="iconfont icon-tags"></i>
                    
                      <a class="hover-with-bg" href="/crystal575/tags/%E9%9D%A2%E7%BB%8F-python/">面经 python</a>
                    
                  </div>
                
              </div>
              
                <p class="note note-warning">
                  
                    本博客所有文章除特别声明外，均采用 <a target="_blank" href="https://creativecommons.org/licenses/by-sa/4.0/deed.zh" rel="nofollow noopener noopener">CC BY-SA 4.0 协议</a> ，转载请注明出处！
                  
                </p>
              
              
                <div class="post-prevnext">
                  <article class="post-prev col-6">
                    
                    
                      <a href="/crystal575/2021/11/13/golang%E7%BC%96%E7%A8%8B%E6%80%9D%E6%83%B3/">
                        <i class="iconfont icon-arrowleft"></i>
                        <span class="hidden-mobile">golanggolang编程思想</span>
                        <span class="visible-mobile">上一篇</span>
                      </a>
                    
                  </article>
                  <article class="post-next col-6">
                    
                    
                  </article>
                </div>
              
            </div>

            
          </article>
        </div>
      </div>
    </div>
    
      <div class="d-none d-lg-block col-lg-2 toc-container" id="toc-ctn">
        <div id="toc">
  <p class="toc-header"><i class="iconfont icon-list"></i>&nbsp;目录</p>
  <div class="toc-body" id="toc-body"></div>
</div>

      </div>
    
  </div>
</div>

<!-- Custom -->


    

    
      <a id="scroll-top-button" href="#" role="button">
        <i class="iconfont icon-arrowup" aria-hidden="true"></i>
      </a>
    

    
      <div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel"
     aria-hidden="true">
  <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
    <div class="modal-content">
      <div class="modal-header text-center">
        <h4 class="modal-title w-100 font-weight-bold">搜索</h4>
        <button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body mx-3">
        <div class="md-form mb-5">
          <input type="text" id="local-search-input" class="form-control validate">
          <label data-error="x" data-success="v"
                 for="local-search-input">关键词</label>
        </div>
        <div class="list-group" id="local-search-result"></div>
      </div>
    </div>
  </div>
</div>
    

    
  </main>

  <footer class="text-center mt-5 py-3">
  <div class="footer-content">
    
  </div>
  
  <div class="statistics">
    
    

    
      
        <!-- 不蒜子统计PV -->
        <span id="busuanzi_container_site_pv" style="display: none">
            总访问量 
            <span id="busuanzi_value_site_pv"></span>
             次
          </span>
      
      
        <!-- 不蒜子统计UV -->
        <span id="busuanzi_container_site_uv" style="display: none">
            总访客数 
            <span id="busuanzi_value_site_uv"></span>
             人
          </span>
      
    
  </div>


  
  <!-- 备案信息 -->
  <div class="beian">
    <span>
      <a href="http://beian.miit.gov.cn/" target="_blank" rel="nofollow noopener">
        京ICP证123456号
      </a>
    </span>
    
      
        <span>
          <a
            href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=12345678"
            rel="nofollow noopener"
            class="beian-police"
            target="_blank"
          >
            
              <span style="visibility: hidden; width: 0">|</span>
              <img src="/crystal575/img/police_beian.png" srcset="/crystal575/img/loading.gif" lazyload alt="police-icon"/>
            
            <span>京公网安备12345678号</span>
          </a>
        </span>
      
    
  </div>


  
</footer>


  <!-- SCRIPTS -->
  
  <script  src="https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js" ></script>
  <link  rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.css" />

  <script>
    NProgress.configure({"showSpinner":false,"trickleSpeed":100})
    NProgress.start()
    window.addEventListener('load', function() {
      NProgress.done();
    })
  </script>


<script  src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js" ></script>
<script  src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js" ></script>
<script  src="/crystal575/js/events.js" ></script>
<script  src="/crystal575/js/plugins.js" ></script>

<!-- Plugins -->


  
    <script  src="/crystal575/js/img-lazyload.js" ></script>
  



  



  <script  src="https://cdn.jsdelivr.net/npm/tocbot@4.12.3/dist/tocbot.min.js" ></script>



  <script  src="https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js" ></script>



  <script  src="https://cdn.jsdelivr.net/npm/anchor-js@4.3.1/anchor.min.js" ></script>



  <script defer src="https://cdn.jsdelivr.net/npm/clipboard@2.0.8/dist/clipboard.min.js" ></script>



  <script  src="/crystal575/js/local-search.js" ></script>



  <script defer src="https://busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" ></script>




  <script  src="https://cdn.jsdelivr.net/npm/typed.js@2.0.12/lib/typed.min.js" ></script>
  <script>
    (function (window, document) {
      var typing = Fluid.plugins.typing;
      var title = document.getElementById('subtitle').title;
      
      typing(title)
      
    })(window, document);
  </script>















<!-- 主题的启动项 保持在最底部 -->
<script  src="/crystal575/js/boot.js" ></script>


</body>
</html>
